tooot/src/Screens.tsx

394 lines
13 KiB
TypeScript
Raw Normal View History

2022-02-14 22:10:07 +01:00
import analytics from '@components/analytics'
import { HeaderLeft } from '@components/Header'
2022-02-01 22:28:12 +01:00
import { displayMessage, Message } from '@components/Message'
2021-05-12 15:40:55 +02:00
import navigationRef from '@helpers/navigationRef'
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
2021-01-30 01:29:15 +01:00
import ScreenActions from '@screens/Actions'
import ScreenAnnouncements from '@screens/Announcements'
import ScreenCompose from '@screens/Compose'
import ScreenImagesViewer from '@screens/ImagesViewer'
import ScreenTabs from '@screens/Tabs'
2022-01-30 22:51:03 +01:00
import initQuery from '@utils/initQuery'
2021-08-29 15:25:38 +02:00
import { RootStackParamList } from '@utils/navigation/navigators'
2021-03-04 01:03:53 +01:00
import pushUseConnect from '@utils/push/useConnect'
import pushUseReceive from '@utils/push/useReceive'
import pushUseRespond from '@utils/push/useRespond'
2021-02-11 23:42:13 +01:00
import { updatePreviousTab } from '@utils/slices/contextsSlice'
2022-02-13 22:14:16 +01:00
import { checkEmojis } from '@utils/slices/instances/checkEmojis'
2021-02-20 19:12:44 +01:00
import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
2021-11-15 22:34:43 +01:00
import { updateConfiguration } from '@utils/slices/instances/updateConfiguration'
2021-05-30 23:39:07 +02:00
import { updateFilters } from '@utils/slices/instances/updateFilters'
2022-01-30 22:51:03 +01:00
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
2021-01-30 01:29:15 +01:00
import { useTheme } from '@utils/styles/ThemeManager'
import { themes } from '@utils/styles/themes'
import * as Linking from 'expo-linking'
2021-02-27 16:33:54 +01:00
import { addScreenshotListener } from 'expo-screen-capture'
import React, { useCallback, useEffect, useRef, useState } from 'react'
2021-01-30 01:29:15 +01:00
import { useTranslation } from 'react-i18next'
import { IntlProvider } from 'react-intl'
2021-02-28 17:41:21 +01:00
import { Alert, Platform, StatusBar } from 'react-native'
2022-05-02 22:31:22 +02:00
import ShareMenu from 'react-native-share-menu'
2022-04-30 23:47:52 +02:00
import { useSelector } from 'react-redux'
2021-02-27 16:33:54 +01:00
import * as Sentry from 'sentry-expo'
2022-04-30 23:47:52 +02:00
import { useAppDispatch } from './store'
2021-01-30 01:29:15 +01:00
2021-08-29 15:25:38 +02:00
const Stack = createNativeStackNavigator<RootStackParamList>()
2021-01-30 01:29:15 +01:00
export interface Props {
localCorrupt?: string
}
const Screens: React.FC<Props> = ({ localCorrupt }) => {
const { i18n, t } = useTranslation('screens')
2022-04-30 23:47:52 +02:00
const dispatch = useAppDispatch()
2021-02-20 19:12:44 +01:00
const instanceActive = useSelector(getInstanceActive)
2022-02-17 00:09:19 +01:00
const { colors, theme } = useTheme()
2021-01-30 01:29:15 +01:00
const routeRef = useRef<{ name?: string; params?: {} }>()
2021-01-30 01:29:15 +01:00
2021-03-04 01:03:53 +01:00
// Push hooks
const instances = useSelector(
getInstances,
(prev, next) => prev.length === next.length
)
2022-01-30 22:51:03 +01:00
pushUseConnect({ t, instances })
pushUseReceive({ instances })
pushUseRespond({ instances })
2021-02-28 17:41:21 +01:00
2021-02-05 01:13:57 +01:00
// Prevent screenshot alert
2021-02-27 16:33:54 +01:00
useEffect(() => {
const screenshotListener = addScreenshotListener(() =>
Alert.alert(t('screenshot.title'), t('screenshot.message'), [
{ text: t('screenshot.button'), style: 'destructive' }
])
)
Platform.select({ ios: screenshotListener })
return () => screenshotListener.remove()
}, [])
2021-02-05 01:13:57 +01:00
2021-01-30 01:29:15 +01:00
// On launch display login credentials corrupt information
useEffect(() => {
2021-02-11 23:42:13 +01:00
const showLocalCorrect = () => {
if (localCorrupt) {
2021-02-28 22:49:55 +01:00
displayMessage({
2021-03-28 23:31:10 +02:00
message: t('localCorrupt.message'),
2021-01-30 01:29:15 +01:00
description: localCorrupt.length ? localCorrupt : undefined,
2021-02-28 22:49:55 +01:00
type: 'error',
2022-02-12 14:51:01 +01:00
theme
2021-01-30 01:29:15 +01:00
})
2022-02-12 14:51:01 +01:00
// @ts-ignore
2021-08-29 15:25:38 +02:00
navigationRef.navigate('Screen-Tabs', {
2021-02-11 23:42:13 +01:00
screen: 'Tab-Me'
})
}
}
return showLocalCorrect()
2021-01-30 01:29:15 +01:00
}, [localCorrupt])
// Lazily update users's preferences, for e.g. composing default visibility
useEffect(() => {
2021-02-20 19:12:44 +01:00
if (instanceActive !== -1) {
2021-11-15 22:34:43 +01:00
dispatch(updateConfiguration())
2021-05-30 23:39:07 +02:00
dispatch(updateFilters())
2021-02-20 19:12:44 +01:00
dispatch(updateAccountPreferences())
2022-02-13 22:14:16 +01:00
dispatch(checkEmojis())
2021-01-30 01:29:15 +01:00
}
}, [instanceActive])
2021-01-30 01:29:15 +01:00
// Callbacks
const navigationContainerOnReady = useCallback(() => {
2021-08-29 15:25:38 +02:00
const currentRoute = navigationRef.getCurrentRoute()
routeRef.current = {
name: currentRoute?.name,
params: currentRoute?.params
2021-03-23 23:16:01 +01:00
? JSON.stringify(currentRoute.params)
: undefined
}
}, [])
2021-01-30 01:29:15 +01:00
const navigationContainerOnStateChange = useCallback(() => {
const previousRoute = routeRef.current
2021-08-29 15:25:38 +02:00
const currentRoute = navigationRef.getCurrentRoute()
2021-01-30 01:29:15 +01:00
const matchTabName = currentRoute?.name?.match(/(Tab-.*)-Root/)
2021-02-11 23:42:13 +01:00
if (matchTabName) {
//@ts-ignore
dispatch(updatePreviousTab(matchTabName[1]))
}
if (previousRoute?.name !== currentRoute?.name) {
2022-02-14 22:10:07 +01:00
analytics('screen_view', { screen_name: currentRoute?.name })
2021-02-27 16:33:54 +01:00
Sentry.Native.setContext('page', {
previous: previousRoute,
current: currentRoute
2021-02-27 16:33:54 +01:00
})
2021-01-30 01:29:15 +01:00
}
routeRef.current = currentRoute
2021-01-30 01:29:15 +01:00
}, [])
// Deep linking for compose
const [deeplinked, setDeeplinked] = useState(false)
useEffect(() => {
const getUrlAsync = async () => {
setDeeplinked(true)
const initialUrl = await Linking.parseInitialURLAsync()
if (initialUrl.path) {
const paths = initialUrl.path.split('/')
if (paths && paths.length) {
const instanceIndex = instances.findIndex(
instance => paths[0] === `@${instance.account.acct}@${instance.uri}`
)
if (instanceIndex !== -1 && instanceActive !== instanceIndex) {
2022-01-30 22:51:03 +01:00
initQuery({
instance: instances[instanceIndex],
prefetch: { enabled: true }
})
}
}
}
if (initialUrl.hostname === 'compose') {
navigationRef.navigate('Screen-Compose')
}
}
if (!deeplinked) {
getUrlAsync()
}
}, [instanceActive, instances, deeplinked])
2022-05-02 22:31:22 +02:00
// Share Extension
const handleShare = useCallback(
2022-05-05 23:03:00 +02:00
(
item?:
| {
data: { mimeType: string; data: string }[]
mimeType: undefined
}
| { data: string | string[]; mimeType: string }
) => {
2022-05-02 22:31:22 +02:00
if (instanceActive < 0) {
return
}
2022-05-05 23:03:00 +02:00
if (!item || !item.data) {
2022-05-02 22:31:22 +02:00
return
}
let text: string | undefined = undefined
let images: { type: string; uri: string }[] = []
let video: { type: string; uri: string } | undefined = undefined
2022-05-05 23:03:00 +02:00
switch (Platform.OS) {
case 'ios':
if (!Array.isArray(item.data) || !item.data) {
2022-05-02 22:31:22 +02:00
return
}
2022-05-05 23:03:00 +02:00
item.data.forEach(d => {
if (typeof d === 'string') return
const typesImage = ['png', 'jpg', 'jpeg', 'gif']
const typesVideo = ['mp4', 'm4v', 'mov', 'webm']
const { mimeType, data } = d
if (mimeType.startsWith('image/')) {
if (!typesImage.includes(mimeType.split('/')[1])) {
console.warn(
'Image type not supported:',
mimeType.split('/')[1]
)
2022-05-05 23:08:29 +02:00
displayMessage({
message: t('shareError.imageNotSupported', {
type: mimeType.split('/')[1]
}),
type: 'error',
theme
})
2022-05-05 23:03:00 +02:00
return
}
images.push({ type: mimeType.split('/')[1], uri: data })
} else if (mimeType.startsWith('video/')) {
if (!typesVideo.includes(mimeType.split('/')[1])) {
console.warn(
'Video type not supported:',
mimeType.split('/')[1]
)
2022-05-05 23:08:29 +02:00
displayMessage({
message: t('shareError.videoNotSupported', {
type: mimeType.split('/')[1]
}),
type: 'error',
theme
})
2022-05-05 23:03:00 +02:00
return
}
video = { type: mimeType.split('/')[1], uri: data }
} else {
if (typesImage.includes(data.split('.').pop() || '')) {
images.push({ type: data.split('.').pop()!, uri: data })
return
}
if (typesVideo.includes(data.split('.').pop() || '')) {
video = { type: data.split('.').pop()!, uri: data }
return
}
text = !text ? data : text.concat(text, `\n${data}`)
}
})
break
case 'android':
if (!item.mimeType) {
2022-05-02 22:31:22 +02:00
return
}
2022-05-05 23:03:00 +02:00
let tempData: string[]
if (!Array.isArray(item.data)) {
tempData = [item.data]
} else {
tempData = item.data
2022-05-02 22:31:22 +02:00
}
2022-05-05 23:03:00 +02:00
tempData.forEach(d => {
const typesImage = ['png', 'jpg', 'jpeg', 'gif']
const typesVideo = ['mp4', 'm4v', 'mov', 'webm', 'mpeg']
if (item.mimeType!.startsWith('image/')) {
if (!typesImage.includes(item.mimeType.split('/')[1])) {
console.warn(
'Image type not supported:',
item.mimeType.split('/')[1]
)
2022-05-05 23:08:29 +02:00
displayMessage({
message: t('shareError.imageNotSupported', {
type: item.mimeType.split('/')[1]
}),
type: 'error',
theme
})
2022-05-05 23:03:00 +02:00
return
}
images.push({ type: item.mimeType.split('/')[1], uri: d })
} else if (item.mimeType.startsWith('video/')) {
if (!typesVideo.includes(item.mimeType.split('/')[1])) {
console.warn(
'Video type not supported:',
item.mimeType.split('/')[1]
)
2022-05-05 23:08:29 +02:00
displayMessage({
message: t('shareError.videoNotSupported', {
type: item.mimeType.split('/')[1]
}),
type: 'error',
theme
})
2022-05-05 23:03:00 +02:00
return
}
video = { type: item.mimeType.split('/')[1], uri: d }
} else {
if (typesImage.includes(d.split('.').pop() || '')) {
images.push({ type: d.split('.').pop()!, uri: d })
return
}
if (typesVideo.includes(d.split('.').pop() || '')) {
video = { type: d.split('.').pop()!, uri: d }
return
}
text = !text ? d : text.concat(text, `\n${d}`)
}
})
break
}
if (!text && (!images || !images.length) && !video) {
return
} else {
navigationRef.navigate('Screen-Compose', {
type: 'share',
text,
images,
video
})
}
2022-05-02 22:31:22 +02:00
},
[instanceActive]
)
useEffect(() => {
2022-05-05 23:03:00 +02:00
ShareMenu.getInitialShare(handleShare)
2022-05-02 22:31:22 +02:00
}, [])
useEffect(() => {
2022-05-05 23:03:00 +02:00
const listener = ShareMenu.addNewShareListener(handleShare)
2022-05-02 22:31:22 +02:00
return () => {
listener.remove()
}
}, [])
2021-01-30 01:29:15 +01:00
return (
<IntlProvider locale={i18n.language}>
<StatusBar
{...(Platform.OS === 'ios' && {
backgroundColor: colors.backgroundDefault
})}
/>
2021-01-30 01:29:15 +01:00
<NavigationContainer
ref={navigationRef}
2022-02-12 14:51:01 +01:00
theme={themes[theme]}
2021-01-30 01:29:15 +01:00
onReady={navigationContainerOnReady}
onStateChange={navigationContainerOnStateChange}
>
2021-02-10 00:40:44 +01:00
<Stack.Navigator initialRouteName='Screen-Tabs'>
2021-01-30 01:29:15 +01:00
<Stack.Screen
name='Screen-Tabs'
component={ScreenTabs}
options={{ headerShown: false }}
/>
<Stack.Screen
name='Screen-Actions'
component={ScreenActions}
options={{
presentation: 'transparentModal',
animation: 'fade',
2021-05-09 21:59:03 +02:00
headerShown: false
2021-01-30 01:29:15 +01:00
}}
/>
<Stack.Screen
name='Screen-Announcements'
component={ScreenAnnouncements}
2021-05-12 22:45:51 +02:00
options={({ navigation }) => ({
presentation: 'transparentModal',
animation: 'fade',
2021-05-12 22:45:51 +02:00
headerShown: true,
headerShadowVisible: false,
headerTransparent: true,
2021-05-12 22:45:51 +02:00
headerStyle: { backgroundColor: 'transparent' },
headerLeft: () => (
<HeaderLeft content='X' onPress={() => navigation.goBack()} />
),
2021-12-18 23:44:08 +01:00
title: t('screenAnnouncements:heading')
2021-05-12 22:45:51 +02:00
})}
2021-01-30 01:29:15 +01:00
/>
<Stack.Screen
name='Screen-Compose'
component={ScreenCompose}
2021-02-10 00:40:44 +01:00
options={{
headerShown: false,
presentation: 'fullScreenModal'
2021-02-10 00:40:44 +01:00
}}
2021-01-30 01:29:15 +01:00
/>
<Stack.Screen
name='Screen-ImagesViewer'
component={ScreenImagesViewer}
options={{
headerShown: false,
presentation: 'fullScreenModal',
animation: 'fade'
2021-01-30 01:29:15 +01:00
}}
/>
</Stack.Navigator>
2021-02-28 22:49:55 +01:00
<Message />
2021-01-30 01:29:15 +01:00
</NavigationContainer>
</IntlProvider>
2021-01-30 01:29:15 +01:00
)
}
export default React.memo(Screens, () => true)