mirror of
https://github.com/tooot-app/app
synced 2025-04-02 20:51:28 +02:00
First step to adapt to Android
This commit is contained in:
parent
2df172d026
commit
49715bba0d
26
App.tsx
26
App.tsx
@ -1,4 +1,11 @@
|
|||||||
|
import { ActionSheetProvider } from '@expo/react-native-action-sheet'
|
||||||
import Index from '@root/Index'
|
import Index from '@root/Index'
|
||||||
|
import dev from '@root/startup/dev'
|
||||||
|
import sentry from '@root/startup/sentry'
|
||||||
|
import log from '@root/startup/log'
|
||||||
|
import audio from '@root/startup/audio'
|
||||||
|
import onlineStatus from '@root/startup/onlineStatus'
|
||||||
|
import netInfo from '@root/startup/netInfo'
|
||||||
import { persistor, store } from '@root/store'
|
import { persistor, store } from '@root/store'
|
||||||
import ThemeManager from '@utils/styles/ThemeManager'
|
import ThemeManager from '@utils/styles/ThemeManager'
|
||||||
import * as SplashScreen from 'expo-splash-screen'
|
import * as SplashScreen from 'expo-splash-screen'
|
||||||
@ -7,17 +14,22 @@ import { enableScreens } from 'react-native-screens'
|
|||||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { PersistGate } from 'redux-persist/integration/react'
|
import { PersistGate } from 'redux-persist/integration/react'
|
||||||
import dev from '@root/startup/dev'
|
import { LogBox, Platform } from 'react-native'
|
||||||
import sentry from '@root/startup/sentry'
|
import Moment from 'react-moment'
|
||||||
import log from '@root/startup/log'
|
|
||||||
import audio from '@root/startup/audio'
|
import moment from 'moment'
|
||||||
import onlineStatus from '@root/startup/onlineStatus'
|
import 'moment/min/locales'
|
||||||
import netInfo from '@root/startup/netInfo'
|
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
LogBox.ignoreLogs(['Setting a timer for a long period of time'])
|
||||||
|
}
|
||||||
|
|
||||||
dev()
|
dev()
|
||||||
sentry()
|
sentry()
|
||||||
audio()
|
audio()
|
||||||
onlineStatus()
|
onlineStatus()
|
||||||
|
Moment.globalMoment = moment
|
||||||
|
Moment.startPooledTimer(1000)
|
||||||
|
|
||||||
log('log', 'react-query', 'initializing')
|
log('log', 'react-query', 'initializing')
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
@ -68,9 +80,11 @@ const App: React.FC = () => {
|
|||||||
log('log', 'App', 'loading actual app :)')
|
log('log', 'App', 'loading actual app :)')
|
||||||
require('@root/i18n/i18n')
|
require('@root/i18n/i18n')
|
||||||
return (
|
return (
|
||||||
|
<ActionSheetProvider>
|
||||||
<ThemeManager>
|
<ThemeManager>
|
||||||
<Index localCorrupt={localCorrupt} />
|
<Index localCorrupt={localCorrupt} />
|
||||||
</ThemeManager>
|
</ThemeManager>
|
||||||
|
</ActionSheetProvider>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
|
@ -24,6 +24,10 @@ export default (): ExpoConfig => ({
|
|||||||
},
|
},
|
||||||
googleServicesFile: './configs/GoogleService-Info.plist'
|
googleServicesFile: './configs/GoogleService-Info.plist'
|
||||||
},
|
},
|
||||||
|
android: {
|
||||||
|
package: 'com.xmflsct.app.mastodon',
|
||||||
|
googleServicesFile: './configs/google-services.json'
|
||||||
|
},
|
||||||
// locales: {
|
// locales: {
|
||||||
// zh: {
|
// zh: {
|
||||||
// CFBundleDisplayName: '我的嘟嘟'
|
// CFBundleDisplayName: '我的嘟嘟'
|
||||||
@ -57,8 +61,5 @@ export default (): ExpoConfig => ({
|
|||||||
measurementId: 'G-3J0FS8WV5J'
|
measurementId: 'G-3J0FS8WV5J'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
experiments: {
|
|
||||||
turboModules: true
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Binary file not shown.
46
configs/google-services.json
Normal file
46
configs/google-services.json
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "661638997772",
|
||||||
|
"project_id": "xmflsct-mastodon-app",
|
||||||
|
"storage_bucket": "xmflsct-mastodon-app.appspot.com"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:661638997772:android:0f929e1c9ebd7f969f8b29",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.xmflsct.app.mastodon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "661638997772-6aiqk97aema0rt280i7nfar3ha2mlgno.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyDUw4s-mhQsHvs4hdIsldsi68ZIygM5MC4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "661638997772-6aiqk97aema0rt280i7nfar3ha2mlgno.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_id": "661638997772-sqa4raeghhrieqt9guljhcul9b51dvna.apps.googleusercontent.com",
|
||||||
|
"client_type": 2,
|
||||||
|
"ios_info": {
|
||||||
|
"bundle_id": "com.xmflsct.app.mastodon"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
@ -9,6 +9,7 @@
|
|||||||
"test": "jest --watchAll"
|
"test": "jest --watchAll"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@expo/react-native-action-sheet": "^3.8.0",
|
||||||
"@react-native-async-storage/async-storage": "^1.13.2",
|
"@react-native-async-storage/async-storage": "^1.13.2",
|
||||||
"@react-native-community/masked-view": "0.1.10",
|
"@react-native-community/masked-view": "0.1.10",
|
||||||
"@react-native-community/netinfo": "^5.9.7",
|
"@react-native-community/netinfo": "^5.9.7",
|
||||||
@ -43,10 +44,12 @@
|
|||||||
"gl-react-expo": "^4.0.1",
|
"gl-react-expo": "^4.0.1",
|
||||||
"i18next": "^19.8.4",
|
"i18next": "^19.8.4",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
|
"moment": "^2.29.1",
|
||||||
"pretty-bytes": "^5.5.0",
|
"pretty-bytes": "^5.5.0",
|
||||||
"react": "16.13.1",
|
"react": "16.13.1",
|
||||||
"react-dom": "16.13.1",
|
"react-dom": "16.13.1",
|
||||||
"react-i18next": "^11.8.5",
|
"react-i18next": "^11.8.5",
|
||||||
|
"react-moment": "^1.1.1",
|
||||||
"react-native": "https://github.com/expo/react-native/archive/sdk-40.0.0.tar.gz",
|
"react-native": "https://github.com/expo/react-native/archive/sdk-40.0.0.tar.gz",
|
||||||
"react-native-animated-spinkit": "^1.4.2",
|
"react-native-animated-spinkit": "^1.4.2",
|
||||||
"react-native-expo-image-cache": "^4.1.0",
|
"react-native-expo-image-cache": "^4.1.0",
|
||||||
|
@ -108,7 +108,6 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
refetchIntervalInBackground: true
|
refetchIntervalInBackground: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const prevNotification = useSelector(getLocalNotification)
|
const prevNotification = useSelector(getLocalNotification)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (queryNotification.data?.pages) {
|
if (queryNotification.data?.pages) {
|
||||||
@ -244,7 +243,7 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StatusBar barStyle={barStyle[mode]} />
|
<StatusBar barStyle={barStyle[mode]} backgroundColor={theme.background} />
|
||||||
<NavigationContainer
|
<NavigationContainer
|
||||||
ref={navigationRef}
|
ref={navigationRef}
|
||||||
theme={themes[mode]}
|
theme={themes[mode]}
|
||||||
@ -293,7 +292,7 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
<Tab.Screen name='Screen-Me' component={ScreenMe} />
|
<Tab.Screen name='Screen-Me' component={ScreenMe} />
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
|
|
||||||
<Toast ref={Toast.setRef} config={toastConfig} />
|
{/* <Toast ref={Toast.setRef} config={toastConfig} /> */}
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
|
||||||
import React, { useRef } from 'react'
|
|
||||||
import { RefreshControl } from 'react-native'
|
|
||||||
import { InfiniteData, useQueryClient } from 'react-query'
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
queryKey: QueryKeyTimeline
|
|
||||||
isFetchingPreviousPage: boolean
|
|
||||||
isFetching: boolean
|
|
||||||
fetchPreviousPage: () => void
|
|
||||||
refetch: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const CustomRefreshControl = React.memo(
|
|
||||||
({
|
|
||||||
queryKey,
|
|
||||||
isFetchingPreviousPage,
|
|
||||||
isFetching,
|
|
||||||
fetchPreviousPage,
|
|
||||||
refetch
|
|
||||||
}: Props) => {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const refreshCount = useRef(0)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={
|
|
||||||
refreshCount.current < 2 ? isFetchingPreviousPage : isFetching
|
|
||||||
}
|
|
||||||
onRefresh={async () => {
|
|
||||||
if (refreshCount.current < 2) {
|
|
||||||
await fetchPreviousPage()
|
|
||||||
refreshCount.current++
|
|
||||||
} else {
|
|
||||||
queryClient.setQueryData<InfiniteData<any> | undefined>(
|
|
||||||
queryKey,
|
|
||||||
data => {
|
|
||||||
if (data) {
|
|
||||||
return {
|
|
||||||
pages: data.pages.slice(1),
|
|
||||||
pageParams: data.pageParams.slice(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
await refetch()
|
|
||||||
refreshCount.current = 0
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
(prev, next) => {
|
|
||||||
let skipUpdate = true
|
|
||||||
skipUpdate = prev.isFetchingPreviousPage === next.isFetchingPreviousPage
|
|
||||||
skipUpdate = prev.isFetching === next.isFetching
|
|
||||||
return skipUpdate
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export default CustomRefreshControl
|
|
@ -65,7 +65,7 @@ const MenuRow: React.FC<Props> = ({
|
|||||||
{iconFront && (
|
{iconFront && (
|
||||||
<Icon
|
<Icon
|
||||||
name={iconFront}
|
name={iconFront}
|
||||||
size={StyleConstants.Font.Size.M + 2}
|
size={StyleConstants.Font.Size.L}
|
||||||
color={theme[iconFrontColor]}
|
color={theme[iconFrontColor]}
|
||||||
style={styles.iconFront}
|
style={styles.iconFront}
|
||||||
/>
|
/>
|
||||||
@ -118,7 +118,7 @@ const MenuRow: React.FC<Props> = ({
|
|||||||
<>
|
<>
|
||||||
<Icon
|
<Icon
|
||||||
name={iconBack}
|
name={iconBack}
|
||||||
size={StyleConstants.Font.Size.M + 2}
|
size={StyleConstants.Font.Size.L}
|
||||||
color={theme[iconBackColor]}
|
color={theme[iconBackColor]}
|
||||||
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
|
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
|
||||||
/>
|
/>
|
||||||
@ -139,6 +139,7 @@ const styles = StyleSheet.create({
|
|||||||
core: {
|
core: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||||
paddingRight: StyleConstants.Spacing.Global.PagePadding
|
paddingRight: StyleConstants.Spacing.Global.PagePadding
|
||||||
},
|
},
|
||||||
|
@ -45,12 +45,9 @@ const renderNode = ({
|
|||||||
? routeParams.hashtag !== tag[1] && routeParams.hashtag !== tag[2]
|
? routeParams.hashtag !== tag[1] && routeParams.hashtag !== tag[2]
|
||||||
: true
|
: true
|
||||||
return (
|
return (
|
||||||
<Text
|
<Pressable
|
||||||
key={index}
|
key={index}
|
||||||
style={{
|
hitSlop={StyleConstants.Font.Size[size] / 2}
|
||||||
color: theme.blue,
|
|
||||||
...StyleConstants.FontStyle[size]
|
|
||||||
}}
|
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
!disableDetails &&
|
!disableDetails &&
|
||||||
differentTag &&
|
differentTag &&
|
||||||
@ -58,10 +55,17 @@ const renderNode = ({
|
|||||||
hashtag: tag[1] || tag[2]
|
hashtag: tag[1] || tag[2]
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: theme.blue,
|
||||||
|
...StyleConstants.FontStyle[size]
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{node.children[0].data}
|
{node.children[0].data}
|
||||||
{node.children[1]?.children[0].data}
|
{node.children[1]?.children[0].data}
|
||||||
</Text>
|
</Text>
|
||||||
|
</Pressable>
|
||||||
)
|
)
|
||||||
} else if (classes.includes('mention') && mentions) {
|
} else if (classes.includes('mention') && mentions) {
|
||||||
const accountIndex = mentions.findIndex(
|
const accountIndex = mentions.findIndex(
|
||||||
@ -71,12 +75,9 @@ const renderNode = ({
|
|||||||
? routeParams.account.id !== mentions[accountIndex].id
|
? routeParams.account.id !== mentions[accountIndex].id
|
||||||
: true
|
: true
|
||||||
return (
|
return (
|
||||||
<Text
|
<Pressable
|
||||||
key={index}
|
key={index}
|
||||||
style={{
|
hitSlop={StyleConstants.Font.Size[size] / 2}
|
||||||
color: accountIndex !== -1 ? theme.blue : undefined,
|
|
||||||
...StyleConstants.FontStyle[size]
|
|
||||||
}}
|
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
accountIndex !== -1 &&
|
accountIndex !== -1 &&
|
||||||
!disableDetails &&
|
!disableDetails &&
|
||||||
@ -85,10 +86,17 @@ const renderNode = ({
|
|||||||
account: mentions[accountIndex]
|
account: mentions[accountIndex]
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: accountIndex !== -1 ? theme.blue : undefined,
|
||||||
|
...StyleConstants.FontStyle[size]
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{node.children[0].data}
|
{node.children[0].data}
|
||||||
{node.children[1]?.children[0].data}
|
{node.children[1]?.children[0].data}
|
||||||
</Text>
|
</Text>
|
||||||
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -99,12 +107,9 @@ const renderNode = ({
|
|||||||
const shouldBeTag =
|
const shouldBeTag =
|
||||||
tags && tags.filter(tag => `#${tag.name}` === content).length > 0
|
tags && tags.filter(tag => `#${tag.name}` === content).length > 0
|
||||||
return (
|
return (
|
||||||
<Text
|
<Pressable
|
||||||
key={index}
|
key={index}
|
||||||
style={{
|
hitSlop={StyleConstants.Font.Size[size] / 2}
|
||||||
color: theme.blue,
|
|
||||||
...StyleConstants.FontStyle[size]
|
|
||||||
}}
|
|
||||||
onPress={async () =>
|
onPress={async () =>
|
||||||
!disableDetails && !shouldBeTag
|
!disableDetails && !shouldBeTag
|
||||||
? await openLink(href)
|
? await openLink(href)
|
||||||
@ -112,6 +117,12 @@ const renderNode = ({
|
|||||||
hashtag: content.substring(1)
|
hashtag: content.substring(1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: theme.blue,
|
||||||
|
...StyleConstants.FontStyle[size]
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{!shouldBeTag ? (
|
{!shouldBeTag ? (
|
||||||
<Icon
|
<Icon
|
||||||
@ -122,6 +133,7 @@ const renderNode = ({
|
|||||||
) : null}
|
) : null}
|
||||||
{content || (showFullLink ? href : domain[1])}
|
{content || (showFullLink ? href : domain[1])}
|
||||||
</Text>
|
</Text>
|
||||||
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@ -206,7 +218,7 @@ const ParseHTML: React.FC<Props> = ({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View style={{ overflow: 'hidden' }}>
|
||||||
<Text
|
<Text
|
||||||
children={children}
|
children={children}
|
||||||
onTextLayout={onTextLayout}
|
onTextLayout={onTextLayout}
|
||||||
@ -222,14 +234,21 @@ const ParseHTML: React.FC<Props> = ({
|
|||||||
layoutAnimation()
|
layoutAnimation()
|
||||||
setExpanded(!expanded)
|
setExpanded(!expanded)
|
||||||
}}
|
}}
|
||||||
style={{ marginTop: expanded ? 0 : -lineHeight * 2.25 }}
|
style={{
|
||||||
|
marginTop: expanded
|
||||||
|
? 0
|
||||||
|
: -lineHeight * (numberOfLines === 0 ? 1 : 2)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[
|
colors={[
|
||||||
theme.backgroundGradientStart,
|
theme.backgroundGradientStart,
|
||||||
theme.backgroundGradientEnd
|
theme.backgroundGradientEnd
|
||||||
]}
|
]}
|
||||||
locations={[0, lineHeight / (StyleConstants.Font.Size.S * 4)]}
|
locations={[
|
||||||
|
0,
|
||||||
|
lineHeight / (StyleConstants.Font.Size[size] * 5)
|
||||||
|
]}
|
||||||
style={{
|
style={{
|
||||||
paddingTop: StyleConstants.Font.Size.S * 2,
|
paddingTop: StyleConstants.Font.Size.S * 2,
|
||||||
paddingBottom: StyleConstants.Font.Size.S
|
paddingBottom: StyleConstants.Font.Size.S
|
||||||
|
@ -69,7 +69,9 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack.Navigator screenOptions={{ headerHideShadow: true }}>
|
<Stack.Navigator
|
||||||
|
screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}
|
||||||
|
>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
name={`Screen-${name}-Root`}
|
name={`Screen-${name}-Root`}
|
||||||
|
@ -9,12 +9,13 @@ import { useScrollToTop } from '@react-navigation/native'
|
|||||||
import { localUpdateNotification } from '@utils/slices/instancesSlice'
|
import { localUpdateNotification } from '@utils/slices/instancesSlice'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
import { StyleSheet } from 'react-native'
|
import { RefreshControl, StyleSheet } from 'react-native'
|
||||||
import { FlatList } from 'react-native-gesture-handler'
|
import { FlatList } from 'react-native-gesture-handler'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||||
import { findIndex } from 'lodash'
|
import { findIndex } from 'lodash'
|
||||||
import CustomRefreshControl from '@components/CustomRefreshControl'
|
import CustomRefreshControl from '@components/CustomRefreshControl'
|
||||||
|
import { InfiniteData, useQueryClient } from 'react-query'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
page: App.Pages
|
page: App.Pages
|
||||||
@ -156,14 +157,35 @@ const Timeline: React.FC<Props> = ({
|
|||||||
() => <TimelineEnd hasNextPage={!disableInfinity ? hasNextPage : false} />,
|
() => <TimelineEnd hasNextPage={!disableInfinity ? hasNextPage : false} />,
|
||||||
[hasNextPage]
|
[hasNextPage]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const refreshCount = useRef(0)
|
||||||
const refreshControl = useMemo(
|
const refreshControl = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<CustomRefreshControl
|
<RefreshControl
|
||||||
queryKey={queryKey}
|
refreshing={
|
||||||
isFetchingPreviousPage={isFetchingNextPage}
|
refreshCount.current < 2 ? isFetchingPreviousPage : isFetching
|
||||||
isFetching={isFetching}
|
}
|
||||||
fetchPreviousPage={fetchPreviousPage}
|
onRefresh={async () => {
|
||||||
refetch={refetch}
|
if (refreshCount.current < 2) {
|
||||||
|
await fetchPreviousPage()
|
||||||
|
refreshCount.current++
|
||||||
|
} else {
|
||||||
|
queryClient.setQueryData<InfiniteData<any> | undefined>(
|
||||||
|
queryKey,
|
||||||
|
data => {
|
||||||
|
if (data) {
|
||||||
|
return {
|
||||||
|
pages: data.pages.slice(1),
|
||||||
|
pageParams: data.pageParams.slice(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await refetch()
|
||||||
|
refreshCount.current = 0
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[isFetchingPreviousPage, isFetching]
|
[isFetchingPreviousPage, isFetching]
|
||||||
@ -199,10 +221,10 @@ const Timeline: React.FC<Props> = ({
|
|||||||
{...(queryKey &&
|
{...(queryKey &&
|
||||||
queryKey[1].page === 'RemotePublic' && { ListHeaderComponent })}
|
queryKey[1].page === 'RemotePublic' && { ListHeaderComponent })}
|
||||||
{...(toot && isSuccess && { onScrollToIndexFailed })}
|
{...(toot && isSuccess && { onScrollToIndexFailed })}
|
||||||
maintainVisibleContentPosition={{
|
// maintainVisibleContentPosition={{
|
||||||
minIndexForVisible: 0,
|
// minIndexForVisible: 0,
|
||||||
autoscrollToTopThreshold: 2
|
// autoscrollToTopThreshold: 2
|
||||||
}}
|
// }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ const TimelineEnd: React.FC<Props> = ({ hasNextPage }) => {
|
|||||||
) : (
|
) : (
|
||||||
<Text style={[styles.text, { color: theme.secondary }]}>
|
<Text style={[styles.text, { color: theme.secondary }]}>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey='timeline:shared.end.message' // optional -> fallbacks to defaults if not provided
|
i18nKey='timeline:shared.end.message'
|
||||||
components={[
|
components={[
|
||||||
<Icon
|
<Icon
|
||||||
name='Coffee'
|
name='Coffee'
|
||||||
|
@ -11,7 +11,14 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import React, { useCallback, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ActionSheetIOS, Pressable, StyleSheet, Text, View } from 'react-native'
|
import {
|
||||||
|
Platform,
|
||||||
|
Pressable,
|
||||||
|
Share,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View
|
||||||
|
} from 'react-native'
|
||||||
import { useQueryClient } from 'react-query'
|
import { useQueryClient } from 'react-query'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -35,12 +42,39 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
|||||||
onSuccess: (_, params) => {
|
onSuccess: (_, params) => {
|
||||||
const theParams = params as MutationVarsTimelineUpdateStatusProperty
|
const theParams = params as MutationVarsTimelineUpdateStatusProperty
|
||||||
if (
|
if (
|
||||||
|
// Un-bookmark from bookmarks page
|
||||||
(queryKey[1].page === 'Bookmarks' &&
|
(queryKey[1].page === 'Bookmarks' &&
|
||||||
theParams.payload.property === 'bookmarked') ||
|
theParams.payload.property === 'bookmarked') ||
|
||||||
|
// Un-favourite from favourites page
|
||||||
(queryKey[1].page === 'Favourites' &&
|
(queryKey[1].page === 'Favourites' &&
|
||||||
theParams.payload.property === 'favourited')
|
theParams.payload.property === 'favourited') ||
|
||||||
|
// Un-reblog from following page
|
||||||
|
(queryKey[1].page === 'Following' &&
|
||||||
|
theParams.payload.property === 'reblogged' &&
|
||||||
|
theParams.payload.currentValue === true)
|
||||||
) {
|
) {
|
||||||
queryClient.invalidateQueries(queryKey)
|
queryClient.invalidateQueries(queryKey)
|
||||||
|
} else if (theParams.payload.property === 'reblogged') {
|
||||||
|
// When reblogged, update cache of following page
|
||||||
|
const tempQueryKey: QueryKeyTimeline = [
|
||||||
|
'Timeline',
|
||||||
|
{ page: 'Following' }
|
||||||
|
]
|
||||||
|
queryClient.invalidateQueries(tempQueryKey)
|
||||||
|
} else if (theParams.payload.property === 'favourited') {
|
||||||
|
// When favourited, update favourited page
|
||||||
|
const tempQueryKey: QueryKeyTimeline = [
|
||||||
|
'Timeline',
|
||||||
|
{ page: 'Favourites' }
|
||||||
|
]
|
||||||
|
queryClient.invalidateQueries(tempQueryKey)
|
||||||
|
} else if (theParams.payload.property === 'bookmarked') {
|
||||||
|
// When bookmarked, update bookmark page
|
||||||
|
const tempQueryKey: QueryKeyTimeline = [
|
||||||
|
'Timeline',
|
||||||
|
{ page: 'Bookmarks' }
|
||||||
|
]
|
||||||
|
queryClient.invalidateQueries(tempQueryKey)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (err: any, params, oldData) => {
|
onError: (err: any, params, oldData) => {
|
||||||
@ -115,23 +149,18 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
|||||||
}),
|
}),
|
||||||
[status.bookmarked]
|
[status.bookmarked]
|
||||||
)
|
)
|
||||||
const onPressShare = useCallback(
|
const onPressShare = useCallback(() => {
|
||||||
() =>
|
switch (Platform.OS) {
|
||||||
ActionSheetIOS.showShareActionSheetWithOptions(
|
case 'ios':
|
||||||
{
|
return Share.share({
|
||||||
url: status.uri,
|
url: status.uri
|
||||||
excludedActivityTypes: [
|
})
|
||||||
'com.apple.UIKit.activity.Mail',
|
case 'android':
|
||||||
'com.apple.UIKit.activity.Print',
|
return Share.share({
|
||||||
'com.apple.UIKit.activity.SaveToCameraRoll',
|
message: status.uri
|
||||||
'com.apple.UIKit.activity.OpenInIBooks'
|
})
|
||||||
]
|
}
|
||||||
},
|
}, [])
|
||||||
() => haptics('Error'),
|
|
||||||
() => haptics('Success')
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const childrenReply = useMemo(
|
const childrenReply = useMemo(
|
||||||
() => (
|
() => (
|
||||||
@ -139,7 +168,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
|||||||
<Icon
|
<Icon
|
||||||
name='MessageCircle'
|
name='MessageCircle'
|
||||||
color={iconColor}
|
color={iconColor}
|
||||||
size={StyleConstants.Font.Size.M + 2}
|
size={StyleConstants.Font.Size.L}
|
||||||
/>
|
/>
|
||||||
{status.replies_count > 0 && (
|
{status.replies_count > 0 && (
|
||||||
<Text
|
<Text
|
||||||
@ -165,7 +194,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
|||||||
? theme.disabled
|
? theme.disabled
|
||||||
: iconColorAction(status.reblogged)
|
: iconColorAction(status.reblogged)
|
||||||
}
|
}
|
||||||
size={StyleConstants.Font.Size.M + 2}
|
size={StyleConstants.Font.Size.L}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[status.reblogged]
|
[status.reblogged]
|
||||||
@ -175,7 +204,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
|||||||
<Icon
|
<Icon
|
||||||
name='Heart'
|
name='Heart'
|
||||||
color={iconColorAction(status.favourited)}
|
color={iconColorAction(status.favourited)}
|
||||||
size={StyleConstants.Font.Size.M + 2}
|
size={StyleConstants.Font.Size.L}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[status.favourited]
|
[status.favourited]
|
||||||
@ -185,18 +214,14 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
|||||||
<Icon
|
<Icon
|
||||||
name='Bookmark'
|
name='Bookmark'
|
||||||
color={iconColorAction(status.bookmarked)}
|
color={iconColorAction(status.bookmarked)}
|
||||||
size={StyleConstants.Font.Size.M + 2}
|
size={StyleConstants.Font.Size.L}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[status.bookmarked]
|
[status.bookmarked]
|
||||||
)
|
)
|
||||||
const childrenShare = useMemo(
|
const childrenShare = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<Icon
|
<Icon name='Share2' color={iconColor} size={StyleConstants.Font.Size.L} />
|
||||||
name='Share2'
|
|
||||||
color={iconColor}
|
|
||||||
size={StyleConstants.Font.Size.M + 2}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@ -252,6 +277,7 @@ const styles = StyleSheet.create({
|
|||||||
width: '20%',
|
width: '20%',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
paddingVertical: StyleConstants.Spacing.S
|
paddingVertical: StyleConstants.Spacing.S
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -36,7 +36,7 @@ const HeaderActions = React.memo(
|
|||||||
<Icon
|
<Icon
|
||||||
name='MoreHorizontal'
|
name='MoreHorizontal'
|
||||||
color={theme.secondary}
|
color={theme.secondary}
|
||||||
size={StyleConstants.Font.Size.M + 2}
|
size={StyleConstants.Font.Size.L}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
|
@ -65,7 +65,7 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
|
|||||||
<Icon
|
<Icon
|
||||||
name='Trash'
|
name='Trash'
|
||||||
color={theme.secondary}
|
color={theme.secondary}
|
||||||
size={StyleConstants.Font.Size.M + 2}
|
size={StyleConstants.Font.Size.L}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import relativeTime from '@components/relativeTime'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Moment from 'react-moment'
|
||||||
import { StyleSheet, Text } from 'react-native'
|
import { StyleSheet, Text } from 'react-native'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -13,16 +13,10 @@ const HeaderSharedCreated: React.FC<Props> = ({ created_at }) => {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
|
|
||||||
const [since, setSince] = useState(relativeTime(created_at, i18n.language))
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setSince(relativeTime(created_at, i18n.language))
|
|
||||||
}, 1000)
|
|
||||||
return () => clearTimeout(timer)
|
|
||||||
}, [since])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text style={[styles.created_at, { color: theme.secondary }]}>{since}</Text>
|
<Text style={[styles.created_at, { color: theme.secondary }]}>
|
||||||
|
<Moment date={created_at} locale={i18n.language} element={Text} fromNow />
|
||||||
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,8 +10,10 @@ import {
|
|||||||
} from '@utils/queryHooks/timeline'
|
} from '@utils/queryHooks/timeline'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
import { maxBy } from 'lodash'
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import Moment from 'react-moment'
|
||||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||||
import { useQueryClient } from 'react-query'
|
import { useQueryClient } from 'react-query'
|
||||||
|
|
||||||
@ -123,16 +125,24 @@ const TimelinePoll: React.FC<Props> = ({
|
|||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Text style={[styles.expiration, { color: theme.secondary }]}>
|
<Text style={[styles.expiration, { color: theme.secondary }]}>
|
||||||
{t('shared.poll.meta.expiration.until', {
|
<Trans
|
||||||
at: relativeTime(poll.expires_at, i18n.language)
|
i18nKey='timeline:shared.poll.meta.expiration.until'
|
||||||
})}
|
components={[
|
||||||
|
<Moment
|
||||||
|
date={poll.expires_at}
|
||||||
|
locale={i18n.language}
|
||||||
|
element={Text}
|
||||||
|
fromNow
|
||||||
|
/>
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [mode, poll.expired, poll.expires_at])
|
}, [mode, poll.expired, poll.expires_at])
|
||||||
|
|
||||||
const isSelected = useCallback(
|
const isSelected = useCallback(
|
||||||
(index: number): any =>
|
(index: number): string =>
|
||||||
allOptions[index]
|
allOptions[index]
|
||||||
? `Check${poll.multiple ? 'Square' : 'Circle'}`
|
? `Check${poll.multiple ? 'Square' : 'Circle'}`
|
||||||
: `${poll.multiple ? 'Square' : 'Circle'}`,
|
: `${poll.multiple ? 'Square' : 'Circle'}`,
|
||||||
@ -140,6 +150,8 @@ const TimelinePoll: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const pollBodyDisallow = useMemo(() => {
|
const pollBodyDisallow = useMemo(() => {
|
||||||
|
const maxValue = maxBy(poll.options, option => option.votes_count)
|
||||||
|
?.votes_count
|
||||||
return poll.options.map((option, index) => (
|
return poll.options.map((option, index) => (
|
||||||
<View key={index} style={styles.optionContainer}>
|
<View key={index} style={styles.optionContainer}>
|
||||||
<View style={styles.optionContent}>
|
<View style={styles.optionContent}>
|
||||||
@ -152,7 +164,7 @@ const TimelinePoll: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
size={StyleConstants.Font.Size.M}
|
size={StyleConstants.Font.Size.M}
|
||||||
color={
|
color={
|
||||||
poll.own_votes?.includes(index) ? theme.primary : theme.disabled
|
poll.own_votes?.includes(index) ? theme.blue : theme.disabled
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.optionText}>
|
<Text style={styles.optionText}>
|
||||||
@ -160,7 +172,11 @@ const TimelinePoll: React.FC<Props> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
<Text style={[styles.optionPercentage, { color: theme.primary }]}>
|
<Text style={[styles.optionPercentage, { color: theme.primary }]}>
|
||||||
{poll.votes_count
|
{poll.votes_count
|
||||||
? Math.round((option.votes_count / poll.voters_count) * 100)
|
? Math.round(
|
||||||
|
(option.votes_count /
|
||||||
|
(poll.voters_count || poll.votes_count)) *
|
||||||
|
100
|
||||||
|
)
|
||||||
: 0}
|
: 0}
|
||||||
%
|
%
|
||||||
</Text>
|
</Text>
|
||||||
@ -171,9 +187,11 @@ const TimelinePoll: React.FC<Props> = ({
|
|||||||
styles.background,
|
styles.background,
|
||||||
{
|
{
|
||||||
width: `${Math.round(
|
width: `${Math.round(
|
||||||
(option.votes_count / poll.voters_count) * 100
|
(option.votes_count / (poll.voters_count || poll.votes_count)) *
|
||||||
|
100
|
||||||
)}%`,
|
)}%`,
|
||||||
backgroundColor: theme.disabled
|
backgroundColor:
|
||||||
|
option.votes_count === maxValue ? theme.blue : theme.disabled
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@ -221,14 +239,28 @@ const TimelinePoll: React.FC<Props> = ({
|
|||||||
))
|
))
|
||||||
}, [mode, allOptions])
|
}, [mode, allOptions])
|
||||||
|
|
||||||
|
const pollVoteCounts = useMemo(() => {
|
||||||
|
if (poll.voters_count !== null) {
|
||||||
|
return (
|
||||||
|
<Text style={[styles.votes, { color: theme.secondary }]}>
|
||||||
|
{t('shared.poll.meta.count.voters', { count: poll.voters_count })}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
} else if (poll.votes_count !== null) {
|
||||||
|
return (
|
||||||
|
<Text style={[styles.votes, { color: theme.secondary }]}>
|
||||||
|
{t('shared.poll.meta.count.votes', { count: poll.votes_count })}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [poll.voters_count, poll.votes_count])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.base}>
|
<View style={styles.base}>
|
||||||
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}
|
{poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow}
|
||||||
<View style={styles.meta}>
|
<View style={styles.meta}>
|
||||||
{pollButton}
|
{pollButton}
|
||||||
<Text style={[styles.votes, { color: theme.secondary }]}>
|
{pollVoteCounts}
|
||||||
{t('shared.poll.meta.voted', { count: poll.voters_count })}
|
|
||||||
</Text>
|
|
||||||
{pollExpiration}
|
{pollExpiration}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
const relativeTime = (date: string, language: string) => {
|
|
||||||
const units = {
|
|
||||||
year: 24 * 60 * 60 * 1000 * 365,
|
|
||||||
month: (24 * 60 * 60 * 1000 * 365) / 12,
|
|
||||||
day: 24 * 60 * 60 * 1000,
|
|
||||||
hour: 60 * 60 * 1000,
|
|
||||||
minute: 60 * 1000,
|
|
||||||
second: 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
const rtf = new Intl.RelativeTimeFormat(language, {
|
|
||||||
numeric: 'auto'
|
|
||||||
})
|
|
||||||
|
|
||||||
const elapsed = +new Date(date) - +new Date()
|
|
||||||
|
|
||||||
// "Math.abs" accounts for both "past" & "future" scenarios
|
|
||||||
for (const u in units) {
|
|
||||||
// @ts-ignore
|
|
||||||
if (Math.abs(elapsed) > units[u] || u == 'second') {
|
|
||||||
// @ts-ignore
|
|
||||||
return rtf.format(Math.round(elapsed / units[u]), u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default relativeTime
|
|
@ -13,29 +13,24 @@ import { store } from '@root/store'
|
|||||||
if (!getSettingsLanguage(store.getState())) {
|
if (!getSettingsLanguage(store.getState())) {
|
||||||
const deviceLocal = Localization.locale
|
const deviceLocal = Localization.locale
|
||||||
if (deviceLocal.startsWith('zh')) {
|
if (deviceLocal.startsWith('zh')) {
|
||||||
store.dispatch(changeLanguage('zh'))
|
store.dispatch(changeLanguage('zh-CN'))
|
||||||
} else {
|
} else {
|
||||||
store.dispatch(changeLanguage('en'))
|
store.dispatch(changeLanguage('en-US'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i18next.use(initReactI18next).init({
|
i18next.use(initReactI18next).init({
|
||||||
lng: getSettingsLanguage(store.getState()),
|
lng: 'zh-CN',
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en-US',
|
||||||
supportedLngs: ['zh', 'en'],
|
supportedLngs: ['zh-CN', 'en-US'],
|
||||||
nonExplicitSupportedLngs: true,
|
|
||||||
|
|
||||||
ns: ['common'],
|
ns: ['common'],
|
||||||
defaultNS: 'common',
|
defaultNS: 'common',
|
||||||
|
|
||||||
resources: {
|
resources: { 'zh-CN': zh, 'en-US': en },
|
||||||
zh: zh,
|
|
||||||
en: en
|
|
||||||
},
|
|
||||||
|
|
||||||
saveMissing: true,
|
saveMissing: true,
|
||||||
missingKeyHandler: (lng, ns, key, fallbackValue) => {
|
missingKeyHandler: (lng, ns, key, fallbackValue) => {
|
||||||
console.warn('i18n missing: ' + ns + ' : ' + key)
|
console.log('i18n missing: ' + lng + ' - ' + ns + ' : ' + key)
|
||||||
},
|
},
|
||||||
|
|
||||||
// react options
|
// react options
|
||||||
|
@ -123,11 +123,14 @@ export default {
|
|||||||
vote: '投票',
|
vote: '投票',
|
||||||
refresh: '刷新'
|
refresh: '刷新'
|
||||||
},
|
},
|
||||||
|
count: {
|
||||||
|
voters: '已投{{count}}人 • ',
|
||||||
|
votes: '{{count}}票 • '
|
||||||
|
},
|
||||||
expiration: {
|
expiration: {
|
||||||
expired: '投票已结束',
|
expired: '投票已结束',
|
||||||
until: '{{at}}截止'
|
until: '<0 />截止'
|
||||||
},
|
}
|
||||||
voted: '已投{{count}}人 • '
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ const ScreenMe: React.FC = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack.Navigator screenOptions={{ headerHideShadow: true }}>
|
<Stack.Navigator screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Screen-Me-Root'
|
name='Screen-Me-Root'
|
||||||
component={ScreenMeRoot}
|
component={ScreenMeRoot}
|
||||||
|
@ -20,17 +20,13 @@ import { StackScreenProps } from '@react-navigation/stack'
|
|||||||
const ScreenMeRoot: React.FC<StackScreenProps<
|
const ScreenMeRoot: React.FC<StackScreenProps<
|
||||||
Nav.MeStackParamList,
|
Nav.MeStackParamList,
|
||||||
'Screen-Me-Root'
|
'Screen-Me-Root'
|
||||||
>> = ({
|
>> = ({ route: { params }, navigation }) => {
|
||||||
route: {
|
|
||||||
params: { navigateAway }
|
|
||||||
},
|
|
||||||
navigation
|
|
||||||
}) => {
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (navigateAway) {
|
if (params && params.navigateAway) {
|
||||||
navigation.navigate(navigateAway)
|
console.log('oops')
|
||||||
|
navigation.navigate(params.navigateAway)
|
||||||
}
|
}
|
||||||
}, [navigateAway])
|
}, [params])
|
||||||
const localActiveIndex = useSelector(getLocalActiveIndex)
|
const localActiveIndex = useSelector(getLocalActiveIndex)
|
||||||
|
|
||||||
const scrollRef = useRef<Animated.ScrollView>(null)
|
const scrollRef = useRef<Animated.ScrollView>(null)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||||
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import haptics from '@root/components/haptics'
|
import haptics from '@root/components/haptics'
|
||||||
import { persistor } from '@root/store'
|
import { persistor } from '@root/store'
|
||||||
@ -23,11 +24,13 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
|||||||
import prettyBytes from 'pretty-bytes'
|
import prettyBytes from 'pretty-bytes'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ActionSheetIOS, StyleSheet, Text } from 'react-native'
|
import { StyleSheet, Text } from 'react-native'
|
||||||
import { CacheManager } from 'react-native-expo-image-cache'
|
import { CacheManager } from 'react-native-expo-image-cache'
|
||||||
|
import { ScrollView } from 'react-native-gesture-handler'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
const DevDebug: React.FC = () => {
|
const DevDebug: React.FC = () => {
|
||||||
|
const { showActionSheetWithOptions } = useActionSheet()
|
||||||
const localActiveIndex = useSelector(getLocalActiveIndex)
|
const localActiveIndex = useSelector(getLocalActiveIndex)
|
||||||
const localInstances = useSelector(getLocalInstances)
|
const localInstances = useSelector(getLocalInstances)
|
||||||
|
|
||||||
@ -43,7 +46,7 @@ const DevDebug: React.FC = () => {
|
|||||||
content={localInstances.length.toString()}
|
content={localInstances.length.toString()}
|
||||||
iconBack='ChevronRight'
|
iconBack='ChevronRight'
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
ActionSheetIOS.showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
options: localInstances
|
options: localInstances
|
||||||
.map(instance => {
|
.map(instance => {
|
||||||
@ -71,6 +74,7 @@ const DevDebug: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ScreenMeSettings: React.FC = () => {
|
const ScreenMeSettings: React.FC = () => {
|
||||||
|
const { showActionSheetWithOptions } = useActionSheet()
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const { t, i18n } = useTranslation('meSettings')
|
const { t, i18n } = useTranslation('meSettings')
|
||||||
const { setTheme, theme } = useTheme()
|
const { setTheme, theme } = useTheme()
|
||||||
@ -87,15 +91,16 @@ const ScreenMeSettings: React.FC = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ScrollView>
|
||||||
<MenuContainer>
|
<MenuContainer>
|
||||||
<MenuRow
|
<MenuRow
|
||||||
title={t('content.language.heading')}
|
title={t('content.language.heading')}
|
||||||
content={t(`content.language.options.${settingsLanguage}`)}
|
content={t(`content.language.options.${settingsLanguage}`)}
|
||||||
iconBack='ChevronRight'
|
iconBack='ChevronRight'
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
ActionSheetIOS.showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
|
title: t('content.language.heading'),
|
||||||
options: [
|
options: [
|
||||||
t('content.language.options.zh'),
|
t('content.language.options.zh'),
|
||||||
t('content.language.options.en'),
|
t('content.language.options.en'),
|
||||||
@ -107,13 +112,13 @@ const ScreenMeSettings: React.FC = () => {
|
|||||||
switch (buttonIndex) {
|
switch (buttonIndex) {
|
||||||
case 0:
|
case 0:
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
dispatch(changeLanguage('zh'))
|
dispatch(changeLanguage('zh-CN'))
|
||||||
i18n.changeLanguage('zh')
|
i18n.changeLanguage('zh-CN')
|
||||||
break
|
break
|
||||||
case 1:
|
case 1:
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
dispatch(changeLanguage('en'))
|
dispatch(changeLanguage('en-US'))
|
||||||
i18n.changeLanguage('en')
|
i18n.changeLanguage('en-US')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,8 +130,9 @@ const ScreenMeSettings: React.FC = () => {
|
|||||||
content={t(`content.theme.options.${settingsTheme}`)}
|
content={t(`content.theme.options.${settingsTheme}`)}
|
||||||
iconBack='ChevronRight'
|
iconBack='ChevronRight'
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
ActionSheetIOS.showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
|
title: t('content.theme.heading'),
|
||||||
options: [
|
options: [
|
||||||
t('content.theme.options.auto'),
|
t('content.theme.options.auto'),
|
||||||
t('content.theme.options.light'),
|
t('content.theme.options.light'),
|
||||||
@ -161,8 +167,9 @@ const ScreenMeSettings: React.FC = () => {
|
|||||||
content={t(`content.browser.options.${settingsBrowser}`)}
|
content={t(`content.browser.options.${settingsBrowser}`)}
|
||||||
iconBack='ChevronRight'
|
iconBack='ChevronRight'
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
ActionSheetIOS.showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
|
title: t('content.browser.heading'),
|
||||||
options: [
|
options: [
|
||||||
t('content.browser.options.internal'),
|
t('content.browser.options.internal'),
|
||||||
t('content.browser.options.external'),
|
t('content.browser.options.external'),
|
||||||
@ -226,7 +233,7 @@ const ScreenMeSettings: React.FC = () => {
|
|||||||
</MenuContainer>
|
</MenuContainer>
|
||||||
|
|
||||||
{__DEV__ ? <DevDebug /> : null}
|
{__DEV__ ? <DevDebug /> : null}
|
||||||
</>
|
</ScrollView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ const Stack = createNativeStackNavigator()
|
|||||||
|
|
||||||
const ScreenMeSwitch: React.FC = ({ navigation }) => {
|
const ScreenMeSwitch: React.FC = ({ navigation }) => {
|
||||||
return (
|
return (
|
||||||
<Stack.Navigator screenOptions={{ headerHideShadow: true }}>
|
<Stack.Navigator screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Screen-Me-Switch-Root'
|
name='Screen-Me-Switch-Root'
|
||||||
component={ScreenMeSwitchRoot}
|
component={ScreenMeSwitchRoot}
|
||||||
|
@ -16,7 +16,8 @@ const ScreenNotifications: React.FC = () => {
|
|||||||
<Stack.Navigator
|
<Stack.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerTitle: t('notifications:heading'),
|
headerTitle: t('notifications:heading'),
|
||||||
headerHideShadow: true
|
headerHideShadow: true,
|
||||||
|
headerTopInsetEnabled: false
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack.Screen name='Screen-Notifications-Root'>
|
<Stack.Screen name='Screen-Notifications-Root'>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import Timelines from '@components/Timelines'
|
import Timelines from '@components/Timelines'
|
||||||
import { getRemoteUrl } from '@utils/slices/instancesSlice'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
@ -73,18 +73,23 @@ const AccountInformation: React.FC<Props> = ({
|
|||||||
|
|
||||||
<AccountInformationAccount ref={shimmerAccountRef} account={account} />
|
<AccountInformationAccount ref={shimmerAccountRef} account={account} />
|
||||||
|
|
||||||
|
{!ownAccount ? (
|
||||||
|
<>
|
||||||
{account?.fields && account.fields.length > 0 ? (
|
{account?.fields && account.fields.length > 0 ? (
|
||||||
<AccountInformationFields account={account} />
|
<AccountInformationFields account={account} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{account?.note &&
|
{account?.note &&
|
||||||
account.note.length > 0 &&
|
account.note.length > 0 &&
|
||||||
account.note !== '<p></p>' ? (
|
account.note !== '<p></p>' ? (
|
||||||
// Empty notes might generate empty p tag
|
// Empty notes might generate empty p tag
|
||||||
<AccountInformationNotes account={account} />
|
<AccountInformationNotes account={account} />
|
||||||
) : null}
|
) : null}
|
||||||
|
<AccountInformationCreated
|
||||||
<AccountInformationCreated ref={shimmerCreatedRef} account={account} />
|
ref={shimmerCreatedRef}
|
||||||
|
account={account}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<AccountInformationStats ref={shimmerStatsRef} account={account} />
|
<AccountInformationStats ref={shimmerStatsRef} account={account} />
|
||||||
</View>
|
</View>
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import client from '@api/client'
|
|
||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import { ParseHTML } from '@components/Parse'
|
import { ParseHTML } from '@components/Parse'
|
||||||
import relativeTime from '@components/relativeTime'
|
|
||||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'
|
||||||
import {
|
import {
|
||||||
useAnnouncementMutation,
|
useAnnouncementMutation,
|
||||||
@ -12,6 +10,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Moment from 'react-moment'
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Image,
|
Image,
|
||||||
@ -25,33 +24,6 @@ import { FlatList, ScrollView } from 'react-native-gesture-handler'
|
|||||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||||
import { SharedAnnouncementsProp } from './sharedScreens'
|
import { SharedAnnouncementsProp } from './sharedScreens'
|
||||||
|
|
||||||
const fireMutation = async ({
|
|
||||||
announcementId,
|
|
||||||
type,
|
|
||||||
name,
|
|
||||||
me
|
|
||||||
}: {
|
|
||||||
announcementId: Mastodon.Announcement['id']
|
|
||||||
type: 'reaction' | 'dismiss'
|
|
||||||
name?: Mastodon.AnnouncementReaction['name']
|
|
||||||
me?: boolean
|
|
||||||
}) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'reaction':
|
|
||||||
return client<{}>({
|
|
||||||
method: me ? 'delete' : 'put',
|
|
||||||
instance: 'local',
|
|
||||||
url: `announcements/${announcementId}/reactions/${name}`
|
|
||||||
})
|
|
||||||
case 'dismiss':
|
|
||||||
return client<{}>({
|
|
||||||
method: 'post',
|
|
||||||
instance: 'local',
|
|
||||||
url: `announcements/${announcementId}/dismiss`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
|
const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
|
||||||
route: {
|
route: {
|
||||||
params: { showAll = false }
|
params: { showAll = false }
|
||||||
@ -108,7 +80,13 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text style={[styles.published, { color: theme.secondary }]}>
|
<Text style={[styles.published, { color: theme.secondary }]}>
|
||||||
发布于 {relativeTime(item.published_at, i18n.language)}
|
发布于{' '}
|
||||||
|
<Moment
|
||||||
|
date={item.published_at}
|
||||||
|
locale={i18n.language}
|
||||||
|
element={Text}
|
||||||
|
fromNow
|
||||||
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator>
|
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator>
|
||||||
<ParseHTML
|
<ParseHTML
|
||||||
|
@ -214,7 +214,7 @@ const Compose: React.FC<SharedComposeProp> = ({
|
|||||||
edges={hasKeyboard ? ['left', 'right'] : ['left', 'right', 'bottom']}
|
edges={hasKeyboard ? ['left', 'right'] : ['left', 'right', 'bottom']}
|
||||||
>
|
>
|
||||||
<ComposeContext.Provider value={{ composeState, composeDispatch }}>
|
<ComposeContext.Provider value={{ composeState, composeDispatch }}>
|
||||||
<Stack.Navigator>
|
<Stack.Navigator screenOptions={{ headerTopInsetEnabled: false }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Screen-Shared-Compose-Root'
|
name='Screen-Shared-Compose-Root'
|
||||||
component={ComposeRoot}
|
component={ComposeRoot}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useContext, useMemo } from 'react'
|
import React, { useCallback, useContext, useMemo } from 'react'
|
||||||
import { ActionSheetIOS, Pressable, StyleSheet, View } from 'react-native'
|
import { Pressable, StyleSheet, View } from 'react-native'
|
||||||
import addAttachment from './/addAttachment'
|
import addAttachment from './/addAttachment'
|
||||||
import ComposeContext from './utils/createContext'
|
import ComposeContext from './utils/createContext'
|
||||||
|
|
||||||
const ComposeActions: React.FC = () => {
|
const ComposeActions: React.FC = () => {
|
||||||
|
const { showActionSheetWithOptions } = useActionSheet()
|
||||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
@ -24,7 +26,10 @@ const ComposeActions: React.FC = () => {
|
|||||||
if (composeState.poll.active) return
|
if (composeState.poll.active) return
|
||||||
|
|
||||||
if (composeState.attachments.uploads.length < 4) {
|
if (composeState.attachments.uploads.length < 4) {
|
||||||
return await addAttachment({ composeDispatch })
|
return await addAttachment({
|
||||||
|
composeDispatch,
|
||||||
|
showActionSheetWithOptions
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [composeState.poll.active, composeState.attachments.uploads])
|
}, [composeState.poll.active, composeState.attachments.uploads])
|
||||||
|
|
||||||
@ -64,7 +69,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
}, [composeState.visibility])
|
}, [composeState.visibility])
|
||||||
const visibilityOnPress = useCallback(() => {
|
const visibilityOnPress = useCallback(() => {
|
||||||
if (!composeState.visibilityLock) {
|
if (!composeState.visibilityLock) {
|
||||||
ActionSheetIOS.showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
options: ['公开', '不公开', '仅关注着', '私信', '取消'],
|
options: ['公开', '不公开', '仅关注着', '私信', '取消'],
|
||||||
cancelButtonIndex: 4
|
cancelButtonIndex: 4
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import haptics from '@components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
@ -28,6 +29,7 @@ import { ExtendedAttachment } from './utils/types'
|
|||||||
const DEFAULT_HEIGHT = 200
|
const DEFAULT_HEIGHT = 200
|
||||||
|
|
||||||
const ComposeAttachments: React.FC = () => {
|
const ComposeAttachments: React.FC = () => {
|
||||||
|
const { showActionSheetWithOptions } = useActionSheet()
|
||||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
@ -192,7 +194,9 @@ const ComposeAttachments: React.FC = () => {
|
|||||||
backgroundColor: theme.backgroundOverlay
|
backgroundColor: theme.backgroundOverlay
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
onPress={async () => await addAttachment({ composeDispatch })}
|
onPress={async () =>
|
||||||
|
await addAttachment({ composeDispatch, showActionSheetWithOptions })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
type='icon'
|
type='icon'
|
||||||
@ -200,7 +204,9 @@ const ComposeAttachments: React.FC = () => {
|
|||||||
spacing='M'
|
spacing='M'
|
||||||
round
|
round
|
||||||
overlay
|
overlay
|
||||||
onPress={async () => await addAttachment({ composeDispatch })}
|
onPress={async () =>
|
||||||
|
await addAttachment({ composeDispatch, showActionSheetWithOptions })
|
||||||
|
}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top:
|
top:
|
||||||
|
@ -146,7 +146,7 @@ const ComposeEditAttachment: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
|
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
|
||||||
<SafeAreaView style={{ flex: 1 }} edges={['left', 'right', 'bottom']}>
|
<SafeAreaView style={{ flex: 1 }} edges={['left', 'right', 'bottom']}>
|
||||||
<Stack.Navigator>
|
<Stack.Navigator screenOptions={{ headerTopInsetEnabled: false }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Screen-Shared-Compose-EditAttachment-Root'
|
name='Screen-Shared-Compose-EditAttachment-Root'
|
||||||
children={children}
|
children={children}
|
||||||
|
@ -65,7 +65,10 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({
|
|||||||
为附件添加文字说明
|
为附件添加文字说明
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.altTextInput, { borderColor: theme.border }]}
|
style={[
|
||||||
|
styles.altTextInput,
|
||||||
|
{ borderColor: theme.border, color: theme.primary }
|
||||||
|
]}
|
||||||
autoCapitalize='none'
|
autoCapitalize='none'
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
maxLength={1500}
|
maxLength={1500}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import Button from '@components/Button'
|
import Button from '@components/Button'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { MenuRow } from '@components/Menu'
|
import { MenuRow } from '@components/Menu'
|
||||||
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext, useEffect, useState } from 'react'
|
import React, { useContext, useEffect, useState } from 'react'
|
||||||
import { ActionSheetIOS, StyleSheet, TextInput, View } from 'react-native'
|
import { StyleSheet, TextInput, View } from 'react-native'
|
||||||
import ComposeContext from './utils/createContext'
|
import ComposeContext from './utils/createContext'
|
||||||
|
|
||||||
const ComposePoll: React.FC = () => {
|
const ComposePoll: React.FC = () => {
|
||||||
|
const { showActionSheetWithOptions } = useActionSheet()
|
||||||
const {
|
const {
|
||||||
composeState: {
|
composeState: {
|
||||||
poll: { total, options, multiple, expire }
|
poll: { total, options, multiple, expire }
|
||||||
@ -111,7 +113,7 @@ const ComposePoll: React.FC = () => {
|
|||||||
title='可选项'
|
title='可选项'
|
||||||
content={multiple ? '多选' : '单选'}
|
content={multiple ? '多选' : '单选'}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
ActionSheetIOS.showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
options: ['单选', '多选', '取消'],
|
options: ['单选', '多选', '取消'],
|
||||||
cancelButtonIndex: 2
|
cancelButtonIndex: 2
|
||||||
@ -130,7 +132,7 @@ const ComposePoll: React.FC = () => {
|
|||||||
title='有效期'
|
title='有效期'
|
||||||
content={expireMapping[expire]}
|
content={expireMapping[expire]}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
ActionSheetIOS.showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
options: [...Object.values(expireMapping), '取消'],
|
options: [...Object.values(expireMapping), '取消'],
|
||||||
cancelButtonIndex: 7
|
cancelButtonIndex: 7
|
||||||
|
@ -4,14 +4,16 @@ import * as Crypto from 'expo-crypto'
|
|||||||
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
|
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
|
||||||
import * as VideoThumbnails from 'expo-video-thumbnails'
|
import * as VideoThumbnails from 'expo-video-thumbnails'
|
||||||
import { Dispatch } from 'react'
|
import { Dispatch } from 'react'
|
||||||
import { ActionSheetIOS, Alert, Linking } from 'react-native'
|
import { Alert, Linking } from 'react-native'
|
||||||
import { ComposeAction } from './utils/types'
|
import { ComposeAction } from './utils/types'
|
||||||
|
import { ActionSheetOptions } from '@expo/react-native-action-sheet'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
composeDispatch: Dispatch<ComposeAction>
|
composeDispatch: Dispatch<ComposeAction>
|
||||||
|
showActionSheetWithOptions: (options: ActionSheetOptions, callback: (i: number) => void) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const addAttachment = async ({ composeDispatch }: Props): Promise<any> => {
|
const addAttachment = async ({ composeDispatch, showActionSheetWithOptions }: Props): Promise<any> => {
|
||||||
const uploadAttachment = async (result: ImageInfo) => {
|
const uploadAttachment = async (result: ImageInfo) => {
|
||||||
const hash = await Crypto.digestStringAsync(
|
const hash = await Crypto.digestStringAsync(
|
||||||
Crypto.CryptoDigestAlgorithm.SHA256,
|
Crypto.CryptoDigestAlgorithm.SHA256,
|
||||||
@ -106,7 +108,7 @@ const addAttachment = async ({ composeDispatch }: Props): Promise<any> => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionSheetIOS.showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
options: ['从相册选取', '现照', '取消'],
|
options: ['从相册选取', '现照', '取消'],
|
||||||
cancelButtonIndex: 2
|
cancelButtonIndex: 2
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import haptics from '@components/haptics'
|
|
||||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { findIndex } from 'lodash'
|
import { findIndex } from 'lodash'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { ActionSheetIOS, Image, StyleSheet, Text } from 'react-native'
|
import {
|
||||||
|
Image,
|
||||||
|
Platform,
|
||||||
|
Share,
|
||||||
|
StyleSheet,
|
||||||
|
Text
|
||||||
|
} from 'react-native'
|
||||||
import ImageViewer from 'react-native-image-zoom-viewer'
|
import ImageViewer from 'react-native-image-zoom-viewer'
|
||||||
import { IImageInfo } from 'react-native-image-zoom-viewer/built/image-viewer.type'
|
import { IImageInfo } from 'react-native-image-zoom-viewer/built/image-viewer.type'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
@ -69,8 +74,18 @@ const ScreenSharedImagesViewer: React.FC<SharedImagesViewerProp> = ({
|
|||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onPress = useCallback(() => {
|
||||||
|
switch (Platform.OS) {
|
||||||
|
case 'ios':
|
||||||
|
return Share.share({ url: imageUrls[currentIndex].url })
|
||||||
|
case 'android':
|
||||||
|
return Share.share({ message: imageUrls[currentIndex].url })
|
||||||
|
}
|
||||||
|
}, [currentIndex])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack.Navigator screenOptions={{ headerHideShadow: true }}>
|
<Stack.Navigator screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Screen-Shared-ImagesViewer-Root'
|
name='Screen-Shared-ImagesViewer-Root'
|
||||||
component={component}
|
component={component}
|
||||||
@ -85,20 +100,7 @@ const ScreenSharedImagesViewer: React.FC<SharedImagesViewerProp> = ({
|
|||||||
{currentIndex + 1} / {imageUrls.length}
|
{currentIndex + 1} / {imageUrls.length}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
headerRight: () => (
|
headerRight: () => <HeaderRight content='Share' onPress={onPress} />
|
||||||
<HeaderRight
|
|
||||||
content='Share'
|
|
||||||
onPress={() =>
|
|
||||||
ActionSheetIOS.showShareActionSheetWithOptions(
|
|
||||||
{
|
|
||||||
url: imageUrls[currentIndex].url
|
|
||||||
},
|
|
||||||
() => haptics('Error'),
|
|
||||||
() => haptics('Success')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
|
@ -6,12 +6,12 @@ const dev = () => {
|
|||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
Analytics.setDebugModeEnabled(true)
|
Analytics.setDebugModeEnabled(true)
|
||||||
|
|
||||||
log('log', 'devs', 'initializing wdyr')
|
// log('log', 'devs', 'initializing wdyr')
|
||||||
const whyDidYouRender = require('@welldone-software/why-did-you-render')
|
// const whyDidYouRender = require('@welldone-software/why-did-you-render')
|
||||||
whyDidYouRender(React, {
|
// whyDidYouRender(React, {
|
||||||
trackHooks: true,
|
// trackHooks: true,
|
||||||
hotReloadBufferMs: 1000
|
// hotReloadBufferMs: 1000
|
||||||
})
|
// })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { RootState } from '@root/store'
|
|||||||
import * as Analytics from 'expo-firebase-analytics'
|
import * as Analytics from 'expo-firebase-analytics'
|
||||||
|
|
||||||
export type SettingsState = {
|
export type SettingsState = {
|
||||||
language: 'zh' | 'en' | undefined
|
language: 'zh-CN' | 'en-US' | undefined
|
||||||
theme: 'light' | 'dark' | 'auto'
|
theme: 'light' | 'dark' | 'auto'
|
||||||
browser: 'internal' | 'external'
|
browser: 'internal' | 'external'
|
||||||
analytics: boolean
|
analytics: boolean
|
||||||
|
20
yarn.lock
20
yarn.lock
@ -1115,6 +1115,14 @@
|
|||||||
"@expo/config" "3.3.22"
|
"@expo/config" "3.3.22"
|
||||||
metro-react-native-babel-transformer "^0.58.0"
|
metro-react-native-babel-transformer "^0.58.0"
|
||||||
|
|
||||||
|
"@expo/react-native-action-sheet@^3.8.0":
|
||||||
|
version "3.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@expo/react-native-action-sheet/-/react-native-action-sheet-3.8.0.tgz#0db8b70ea8550ceb2983abda8584efa3a61d7389"
|
||||||
|
integrity sha512-tCfwysuqy0sfaN+aA98IKUrwCLKsbDHSYLcnHrx9wNbawOHNez8rSeFtieAS48/HyrPI75yg/ZGvxe6UsJRS8Q==
|
||||||
|
dependencies:
|
||||||
|
"@types/hoist-non-react-statics" "^3.3.1"
|
||||||
|
hoist-non-react-statics "^3.3.0"
|
||||||
|
|
||||||
"@expo/spawn-async@^1.2.8":
|
"@expo/spawn-async@^1.2.8":
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@expo/spawn-async/-/spawn-async-1.5.0.tgz#799827edd8c10ef07eb1a2ff9dcfe081d596a395"
|
resolved "https://registry.yarnpkg.com/@expo/spawn-async/-/spawn-async-1.5.0.tgz#799827edd8c10ef07eb1a2ff9dcfe081d596a395"
|
||||||
@ -2091,7 +2099,7 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.36.tgz#17ce0a235e9ffbcdcdf5095646b374c2bf615a4c"
|
resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.36.tgz#17ce0a235e9ffbcdcdf5095646b374c2bf615a4c"
|
||||||
integrity sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ==
|
integrity sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ==
|
||||||
|
|
||||||
"@types/hoist-non-react-statics@^3.3.0":
|
"@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1":
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
||||||
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
|
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
|
||||||
@ -7307,6 +7315,11 @@ mkdirp@^1.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||||
|
|
||||||
|
moment@^2.29.1:
|
||||||
|
version "2.29.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||||
|
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||||
|
|
||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
@ -8173,6 +8186,11 @@ react-is@^17.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
|
||||||
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
|
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
|
||||||
|
|
||||||
|
react-moment@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-moment/-/react-moment-1.1.1.tgz#5fe9fb257039590c804e2b3aedfc3ceb0a6ffb16"
|
||||||
|
integrity sha512-WjwvxBSnmLMRcU33do0KixDB+9vP3e84eCse+rd+HNklAMNWyRgZTDEQlay/qK6lcXFPRuEIASJTpEt6pyK7Ww==
|
||||||
|
|
||||||
react-native-animated-spinkit@^1.4.2:
|
react-native-animated-spinkit@^1.4.2:
|
||||||
version "1.4.2"
|
version "1.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-animated-spinkit/-/react-native-animated-spinkit-1.4.2.tgz#cb60ff8bcc2bb848409d9aa85ed528646ad1f953"
|
resolved "https://registry.yarnpkg.com/react-native-animated-spinkit/-/react-native-animated-spinkit-1.4.2.tgz#cb60ff8bcc2bb848409d9aa85ed528646ad1f953"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user