import { HeaderLeft } from '@components/Header' import { displayMessage, Message } from '@components/Message' import navigationRef from '@helpers/navigationRef' import { NavigationContainer } from '@react-navigation/native' import { createNativeStackNavigator } from '@react-navigation/native-stack' import ScreenAccountSelection from '@screens/AccountSelection' 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' import * as Sentry from '@sentry/react-native' import initQuery from '@utils/initQuery' import { RootStackParamList } from '@utils/navigation/navigators' import pushUseConnect from '@utils/push/useConnect' import pushUseReceive from '@utils/push/useReceive' import pushUseRespond from '@utils/push/useRespond' import { updatePreviousTab } from '@utils/slices/contextsSlice' import { checkEmojis } from '@utils/slices/instances/checkEmojis' import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences' import { updateConfiguration } from '@utils/slices/instances/updateConfiguration' import { updateFilters } from '@utils/slices/instances/updateFilters' import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice' import { useTheme } from '@utils/styles/ThemeManager' import { themes } from '@utils/styles/themes' import * as Linking from 'expo-linking' import { addScreenshotListener } from 'expo-screen-capture' import React, { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Alert, Platform, StatusBar } from 'react-native' import ShareMenu from 'react-native-share-menu' import { useSelector } from 'react-redux' import { useAppDispatch } from './store' const Stack = createNativeStackNavigator() export interface Props { localCorrupt?: string } const Screens: React.FC = ({ localCorrupt }) => { const { i18n, t } = useTranslation('screens') const dispatch = useAppDispatch() const instanceActive = useSelector(getInstanceActive) const { colors, theme } = useTheme() const routeRef = useRef<{ name?: string; params?: {} }>() // Push hooks const instances = useSelector(getInstances, (prev, next) => prev.length === next.length) pushUseConnect() pushUseReceive() pushUseRespond() // Prevent screenshot alert 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() }, []) // On launch display login credentials corrupt information useEffect(() => { const showLocalCorrect = () => { if (localCorrupt) { displayMessage({ message: t('localCorrupt.message'), description: localCorrupt.length ? localCorrupt : undefined, type: 'error', theme }) // @ts-ignore navigationRef.navigate('Screen-Tabs', { screen: 'Tab-Me' }) } } return showLocalCorrect() }, [localCorrupt]) // Lazily update users's preferences, for e.g. composing default visibility useEffect(() => { if (instanceActive !== -1) { dispatch(updateConfiguration()) dispatch(updateFilters()) dispatch(updateAccountPreferences()) dispatch(checkEmojis()) } }, [instanceActive]) // Callbacks const navigationContainerOnReady = useCallback(() => { const currentRoute = navigationRef.getCurrentRoute() routeRef.current = { name: currentRoute?.name, params: currentRoute?.params ? JSON.stringify(currentRoute.params) : undefined } }, []) const navigationContainerOnStateChange = useCallback(() => { const previousRoute = routeRef.current const currentRoute = navigationRef.getCurrentRoute() const matchTabName = currentRoute?.name?.match(/(Tab-.*)-Root/) if (matchTabName) { //@ts-ignore dispatch(updatePreviousTab(matchTabName[1])) } if (previousRoute?.name !== currentRoute?.name) { Sentry.setContext('page', { previous: previousRoute, current: currentRoute }) } routeRef.current = currentRoute }, []) // 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) { initQuery({ instance: instances[instanceIndex], prefetch: { enabled: true } }) } } } if (initialUrl.hostname === 'compose') { navigationRef.navigate('Screen-Compose') } } if (!deeplinked) { getUrlAsync() } }, [instanceActive, instances, deeplinked]) // Share Extension const handleShare = useCallback( ( item?: | { data: { mimeType: string; data: string }[] mimeType: undefined } | { data: string | string[]; mimeType: string } ) => { if (instanceActive < 0) { return } if (!item || !item.data) { return } let text: string | undefined = undefined let media: { uri: string; mime: string }[] = [] const typesImage = ['png', 'jpg', 'jpeg', 'gif'] const typesVideo = ['mp4', 'm4v', 'mov', 'webm', 'mpeg'] const filterMedia = ({ uri, mime }: { uri: string; mime: string }) => { if (mime.startsWith('image/')) { if (!typesImage.includes(mime.split('/')[1])) { console.warn('Image type not supported:', mime.split('/')[1]) displayMessage({ message: t('shareError.imageNotSupported', { type: mime.split('/')[1] }), type: 'error', theme }) return } media.push({ uri, mime }) } else if (mime.startsWith('video/')) { if (!typesVideo.includes(mime.split('/')[1])) { console.warn('Video type not supported:', mime.split('/')[1]) displayMessage({ message: t('shareError.videoNotSupported', { type: mime.split('/')[1] }), type: 'error', theme }) return } media.push({ uri, mime }) } else { if (typesImage.includes(uri.split('.').pop() || '')) { media.push({ uri, mime: 'image/jpg' }) return } if (typesVideo.includes(uri.split('.').pop() || '')) { media.push({ uri, mime: 'video/mp4' }) return } text = !text ? uri : text.concat(text, `\n${uri}`) } } switch (Platform.OS) { case 'ios': if (!Array.isArray(item.data) || !item.data) { return } for (const d of item.data) { if (typeof d !== 'string') { filterMedia({ uri: d.data, mime: d.mimeType }) } } break case 'android': if (!item.mimeType) { return } if (Array.isArray(item.data)) { for (const d of item.data) { filterMedia({ uri: d, mime: item.mimeType }) } } else { filterMedia({ uri: item.data, mime: item.mimeType }) } break } if (!text && !media.length) { return } else { if (instances.length > 1) { navigationRef.navigate('Screen-AccountSelection', { share: { text, media } }) } else { navigationRef.navigate('Screen-Compose', { type: 'share', text, media }) } } }, [] ) useEffect(() => { ShareMenu.getInitialShare(handleShare) }, []) useEffect(() => { const listener = ShareMenu.addNewShareListener(handleShare) return () => { listener.remove() } }, []) return ( <> ({ presentation: 'transparentModal', animation: 'fade', headerShown: true, headerShadowVisible: false, headerTransparent: true, headerStyle: { backgroundColor: 'transparent' }, headerLeft: () => navigation.goBack()} />, title: t('screenAnnouncements:heading') })} /> ({ title: t('screenAccountSelection:heading'), headerShadowVisible: false, presentation: 'modal', gestureEnabled: false, headerLeft: () => ( navigation.goBack()} /> ) })} /> ) } export default React.memo(Screens, () => true)