Bluesky app fork with some witchin' additions 💫 forked from jollywhoppers.com/witchsky.app
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107import {type JSX, useCallback, useRef} from 'react'import {Linking} from 'react-native'import * as Notifications from 'expo-notifications'import {i18n, type MessageDescriptor} from '@lingui/core'import {msg} from '@lingui/macro'import { type BottomTabBarProps, createBottomTabNavigator,} from '@react-navigation/bottom-tabs'import { CommonActions, createNavigationContainerRef, DarkTheme, DefaultTheme, type LinkingOptions, NavigationContainer, StackActions,} from '@react-navigation/native'
import {timeout} from '#/lib/async/timeout'import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'import { getNotificationPayload, type NotificationPayload, notificationToURL, storePayloadForAccountSwitch,} from '#/lib/hooks/useNotificationHandler'import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration'import {logger as notyLogger} from '#/lib/notifications/util'import {buildStateObject} from '#/lib/routes/helpers'import { type AllNavigatorParams, type BottomTabNavigatorParams, type FlatNavigatorParams, type HomeTabNavigatorParams, type MessagesTabNavigatorParams, type MyProfileTabNavigatorParams, type NotificationsTabNavigatorParams, type SearchTabNavigatorParams,} from '#/lib/routes/types'import {type RouteParams, type State} from '#/lib/routes/types'import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'import {bskyTitle} from '#/lib/strings/headings'import {logger} from '#/logger'import {isNative, isWeb} from '#/platform/detection'import {useUnreadNotifications} from '#/state/queries/notifications/unread'import {useSession} from '#/state/session'import { shouldRequestEmailConfirmation, snoozeEmailConfirmationPrompt,} from '#/state/shell/reminders'import {CommunityGuidelinesScreen} from '#/view/screens/CommunityGuidelines'import {CopyrightPolicyScreen} from '#/view/screens/CopyrightPolicy'import {DebugModScreen} from '#/view/screens/DebugMod'import {FeedsScreen} from '#/view/screens/Feeds'import {HomeScreen} from '#/view/screens/Home'import {ListsScreen} from '#/view/screens/Lists'import {ModerationBlockedAccounts} from '#/view/screens/ModerationBlockedAccounts'import {ModerationModlistsScreen} from '#/view/screens/ModerationModlists'import {ModerationMutedAccounts} from '#/view/screens/ModerationMutedAccounts'import {NotFoundScreen} from '#/view/screens/NotFound'import {NotificationsScreen} from '#/view/screens/Notifications'import {PostThreadScreen} from '#/view/screens/PostThread'import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'import {ProfileScreen} from '#/view/screens/Profile'import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'import {Storybook} from '#/view/screens/Storybook'import {SupportScreen} from '#/view/screens/Support'import {TermsOfServiceScreen} from '#/view/screens/TermsOfService'import {BottomBar} from '#/view/shell/bottom-bar/BottomBar'import {createNativeStackNavigatorWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth'import {BookmarksScreen} from '#/screens/Bookmarks'import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen'import HashtagScreen from '#/screens/Hashtag'import {LogScreen} from '#/screens/Log'import {MessagesScreen} from '#/screens/Messages/ChatList'import {MessagesConversationScreen} from '#/screens/Messages/Conversation'import {MessagesInboxScreen} from '#/screens/Messages/Inbox'import {MessagesSettingsScreen} from '#/screens/Messages/Settings'import {ModerationScreen} from '#/screens/Moderation'import {Screen as ModerationVerificationSettings} from '#/screens/Moderation/VerificationSettings'import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings'import {NotificationsActivityListScreen} from '#/screens/Notifications/ActivityList'import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'import {PostQuotesScreen} from '#/screens/Post/PostQuotes'import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'import {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed'import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers'import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows'import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'import {ProfileSearchScreen} from '#/screens/Profile/ProfileSearch'import {ProfileListScreen} from '#/screens/ProfileList'import {SavedFeeds} from '#/screens/SavedFeeds'import {SearchScreen} from '#/screens/Search'import {AboutSettingsScreen} from '#/screens/Settings/AboutSettings'import {AccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings'import {AccountSettingsScreen} from '#/screens/Settings/AccountSettings'import {ActivityPrivacySettingsScreen} from '#/screens/Settings/ActivityPrivacySettings'import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords'import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings'import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences'import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences'import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings'import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings'import {LegacyNotificationSettingsScreen} from '#/screens/Settings/LegacyNotificationSettings'import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'import {ActivityNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ActivityNotificationSettings'import {LikeNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/LikeNotificationSettings'import {LikesOnRepostsNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings'import {MentionNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/MentionNotificationSettings'import {MiscellaneousNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings'import {NewFollowerNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/NewFollowerNotificationSettings'import {QuoteNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/QuoteNotificationSettings'import {ReplyNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ReplyNotificationSettings'import {RepostNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostNotificationSettings'import {RepostsOnRepostsNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings'import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings'import {SettingsScreen} from '#/screens/Settings/Settings'import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences'import { StarterPackScreen, StarterPackScreenShort,} from '#/screens/StarterPack/StarterPackScreen'import {Wizard} from '#/screens/StarterPack/Wizard'import TopicScreen from '#/screens/Topic'import {VideoFeed} from '#/screens/VideoFeed'import {type Theme, useTheme} from '#/alf'import { EmailDialogScreenID, useEmailDialogControl,} from '#/components/dialogs/EmailDialog'import {router} from '#/routes'import {Referrer} from '../modules/expo-bluesky-swiss-army'import {useAccountSwitcher} from './lib/hooks/useAccountSwitcher'import {useNonReactiveCallback} from './lib/hooks/useNonReactiveCallback'import {useLoggedOutViewControls} from './state/shell/logged-out'import {useCloseAllActiveElements} from './state/util'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
const HomeTab = createNativeStackNavigatorWithAuth<HomeTabNavigatorParams>()const SearchTab = createNativeStackNavigatorWithAuth<SearchTabNavigatorParams>()const NotificationsTab = createNativeStackNavigatorWithAuth<NotificationsTabNavigatorParams>()const MyProfileTab = createNativeStackNavigatorWithAuth<MyProfileTabNavigatorParams>()const MessagesTab = createNativeStackNavigatorWithAuth<MessagesTabNavigatorParams>()const Flat = createNativeStackNavigatorWithAuth<FlatNavigatorParams>()const Tab = createBottomTabNavigator<BottomTabNavigatorParams>()
/** * These "common screens" are reused across stacks. */function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { const title = (page: MessageDescriptor) => bskyTitle(i18n._(page), unreadCountLabel)
return ( <> <Stack.Screen name="NotFound" getComponent={() => NotFoundScreen} options={{title: title(msg`Not Found`)}} /> <Stack.Screen name="Lists" component={ListsScreen} options={{title: title(msg`Lists`), requireAuth: true}} /> <Stack.Screen name="Moderation" getComponent={() => ModerationScreen} options={{title: title(msg`Moderation`), requireAuth: true}} /> <Stack.Screen name="ModerationModlists" getComponent={() => ModerationModlistsScreen} options={{title: title(msg`Moderation Lists`), requireAuth: true}} /> <Stack.Screen name="ModerationMutedAccounts" getComponent={() => ModerationMutedAccounts} options={{title: title(msg`Muted Accounts`), requireAuth: true}} /> <Stack.Screen name="ModerationBlockedAccounts" getComponent={() => ModerationBlockedAccounts} options={{title: title(msg`Blocked Accounts`), requireAuth: true}} /> <Stack.Screen name="ModerationInteractionSettings" getComponent={() => ModerationInteractionSettings} options={{ title: title(msg`Post Interaction Settings`), requireAuth: true, }} /> <Stack.Screen name="ModerationVerificationSettings" getComponent={() => ModerationVerificationSettings} options={{ title: title(msg`Verification Settings`), requireAuth: true, }} /> <Stack.Screen name="Settings" getComponent={() => SettingsScreen} options={{title: title(msg`Settings`), requireAuth: true}} /> <Stack.Screen name="LanguageSettings" getComponent={() => LanguageSettingsScreen} options={{title: title(msg`Language Settings`), requireAuth: true}} /> <Stack.Screen name="Profile" getComponent={() => ProfileScreen} options={({route}) => ({ title: bskyTitle(`@${route.params.name}`, unreadCountLabel), })} /> <Stack.Screen name="ProfileFollowers" getComponent={() => ProfileFollowersScreen} options={({route}) => ({ title: title(msg`People following @${route.params.name}`), })} /> <Stack.Screen name="ProfileFollows" getComponent={() => ProfileFollowsScreen} options={({route}) => ({ title: title(msg`People followed by @${route.params.name}`), })} /> <Stack.Screen name="ProfileKnownFollowers" getComponent={() => ProfileKnownFollowersScreen} options={({route}) => ({ title: title(msg`Followers of @${route.params.name} that you know`), })} /> <Stack.Screen name="ProfileList" getComponent={() => ProfileListScreen} options={{title: title(msg`List`), requireAuth: true}} /> <Stack.Screen name="ProfileSearch" getComponent={() => ProfileSearchScreen} options={({route}) => ({ title: title(msg`Search @${route.params.name}'s posts`), })} /> <Stack.Screen name="PostThread" getComponent={() => PostThreadScreen} options={({route}) => ({ title: title(msg`Post by @${route.params.name}`), })} /> <Stack.Screen name="PostLikedBy" getComponent={() => PostLikedByScreen} options={({route}) => ({ title: title(msg`Post by @${route.params.name}`), })} /> <Stack.Screen name="PostRepostedBy" getComponent={() => PostRepostedByScreen} options={({route}) => ({ title: title(msg`Post by @${route.params.name}`), })} /> <Stack.Screen name="PostQuotes" getComponent={() => PostQuotesScreen} options={({route}) => ({ title: title(msg`Post by @${route.params.name}`), })} /> <Stack.Screen name="ProfileFeed" getComponent={() => ProfileFeedScreen} options={{title: title(msg`Feed`)}} /> <Stack.Screen name="ProfileFeedLikedBy" getComponent={() => ProfileFeedLikedByScreen} options={{title: title(msg`Liked by`)}} /> <Stack.Screen name="ProfileLabelerLikedBy" getComponent={() => ProfileLabelerLikedByScreen} options={{title: title(msg`Liked by`)}} /> <Stack.Screen name="Debug" getComponent={() => Storybook} options={{title: title(msg`Storybook`), requireAuth: true}} /> <Stack.Screen name="DebugMod" getComponent={() => DebugModScreen} options={{title: title(msg`Moderation states`), requireAuth: true}} /> <Stack.Screen name="SharedPreferencesTester" getComponent={() => SharedPreferencesTesterScreen} options={{title: title(msg`Shared Preferences Tester`)}} /> <Stack.Screen name="Log" getComponent={() => LogScreen} options={{title: title(msg`Log`), requireAuth: true}} /> <Stack.Screen name="Support" getComponent={() => SupportScreen} options={{title: title(msg`Support`)}} /> <Stack.Screen name="PrivacyPolicy" getComponent={() => PrivacyPolicyScreen} options={{title: title(msg`Privacy Policy`)}} /> <Stack.Screen name="TermsOfService" getComponent={() => TermsOfServiceScreen} options={{title: title(msg`Terms of Service`)}} /> <Stack.Screen name="CommunityGuidelines" getComponent={() => CommunityGuidelinesScreen} options={{title: title(msg`Community Guidelines`)}} /> <Stack.Screen name="CopyrightPolicy" getComponent={() => CopyrightPolicyScreen} options={{title: title(msg`Copyright Policy`)}} /> <Stack.Screen name="AppPasswords" getComponent={() => AppPasswordsScreen} options={{title: title(msg`App Passwords`), requireAuth: true}} /> <Stack.Screen name="SavedFeeds" getComponent={() => SavedFeeds} options={{title: title(msg`Edit My Feeds`), requireAuth: true}} /> <Stack.Screen name="PreferencesFollowingFeed" getComponent={() => FollowingFeedPreferencesScreen} options={{ title: title(msg`Following Feed Preferences`), requireAuth: true, }} /> <Stack.Screen name="PreferencesThreads" getComponent={() => ThreadPreferencesScreen} options={{title: title(msg`Threads Preferences`), requireAuth: true}} /> <Stack.Screen name="PreferencesExternalEmbeds" getComponent={() => ExternalMediaPreferencesScreen} options={{ title: title(msg`External Media Preferences`), requireAuth: true, }} /> <Stack.Screen name="AccessibilitySettings" getComponent={() => AccessibilitySettingsScreen} options={{ title: title(msg`Accessibility Settings`), requireAuth: true, }} /> <Stack.Screen name="AppearanceSettings" getComponent={() => AppearanceSettingsScreen} options={{ title: title(msg`Appearance`), requireAuth: true, }} /> <Stack.Screen name="AccountSettings" getComponent={() => AccountSettingsScreen} options={{ title: title(msg`Account`), requireAuth: true, }} /> <Stack.Screen name="PrivacyAndSecuritySettings" getComponent={() => PrivacyAndSecuritySettingsScreen} options={{ title: title(msg`Privacy and Security`), requireAuth: true, }} /> <Stack.Screen name="ActivityPrivacySettings" getComponent={() => ActivityPrivacySettingsScreen} options={{ title: title(msg`Privacy and Security`), requireAuth: true, }} /> <Stack.Screen name="NotificationSettings" getComponent={() => NotificationSettingsScreen} options={{title: title(msg`Notification settings`), requireAuth: true}} /> <Stack.Screen name="ReplyNotificationSettings" getComponent={() => ReplyNotificationSettingsScreen} options={{ title: title(msg`Reply notifications`), requireAuth: true, }} /> <Stack.Screen name="MentionNotificationSettings" getComponent={() => MentionNotificationSettingsScreen} options={{ title: title(msg`Mention notifications`), requireAuth: true, }} /> <Stack.Screen name="QuoteNotificationSettings" getComponent={() => QuoteNotificationSettingsScreen} options={{ title: title(msg`Quote notifications`), requireAuth: true, }} /> <Stack.Screen name="LikeNotificationSettings" getComponent={() => LikeNotificationSettingsScreen} options={{ title: title(msg`Like notifications`), requireAuth: true, }} /> <Stack.Screen name="RepostNotificationSettings" getComponent={() => RepostNotificationSettingsScreen} options={{ title: title(msg`Repost notifications`), requireAuth: true, }} /> <Stack.Screen name="NewFollowerNotificationSettings" getComponent={() => NewFollowerNotificationSettingsScreen} options={{ title: title(msg`New follower notifications`), requireAuth: true, }} /> <Stack.Screen name="LikesOnRepostsNotificationSettings" getComponent={() => LikesOnRepostsNotificationSettingsScreen} options={{ title: title(msg`Likes of your reposts notifications`), requireAuth: true, }} /> <Stack.Screen name="RepostsOnRepostsNotificationSettings" getComponent={() => RepostsOnRepostsNotificationSettingsScreen} options={{ title: title(msg`Reposts of your reposts notifications`), requireAuth: true, }} /> <Stack.Screen name="ActivityNotificationSettings" getComponent={() => ActivityNotificationSettingsScreen} options={{ title: title(msg`Activity notifications`), requireAuth: true, }} /> <Stack.Screen name="MiscellaneousNotificationSettings" getComponent={() => MiscellaneousNotificationSettingsScreen} options={{ title: title(msg`Miscellaneous notifications`), requireAuth: true, }} /> <Stack.Screen name="ContentAndMediaSettings" getComponent={() => ContentAndMediaSettingsScreen} options={{ title: title(msg`Content and Media`), requireAuth: true, }} /> <Stack.Screen name="InterestsSettings" getComponent={() => InterestsSettingsScreen} options={{ title: title(msg`Your interests`), requireAuth: true, }} /> <Stack.Screen name="AboutSettings" getComponent={() => AboutSettingsScreen} options={{ title: title(msg`About`), requireAuth: true, }} /> <Stack.Screen name="AppIconSettings" getComponent={() => AppIconSettingsScreen} options={{ title: title(msg`App Icon`), requireAuth: true, }} /> <Stack.Screen name="Hashtag" getComponent={() => HashtagScreen} options={{title: title(msg`Hashtag`)}} /> <Stack.Screen name="Topic" getComponent={() => TopicScreen} options={{title: title(msg`Topic`)}} /> <Stack.Screen name="MessagesConversation" getComponent={() => MessagesConversationScreen} options={{title: title(msg`Chat`), requireAuth: true}} /> <Stack.Screen name="MessagesSettings" getComponent={() => MessagesSettingsScreen} options={{title: title(msg`Chat settings`), requireAuth: true}} /> <Stack.Screen name="MessagesInbox" getComponent={() => MessagesInboxScreen} options={{title: title(msg`Chat request inbox`), requireAuth: true}} /> <Stack.Screen name="NotificationsActivityList" getComponent={() => NotificationsActivityListScreen} options={{title: title(msg`Notifications`), requireAuth: true}} /> <Stack.Screen name="LegacyNotificationSettings" getComponent={() => LegacyNotificationSettingsScreen} options={{title: title(msg`Notification settings`), requireAuth: true}} /> <Stack.Screen name="Feeds" getComponent={() => FeedsScreen} options={{title: title(msg`Feeds`)}} /> <Stack.Screen name="StarterPack" getComponent={() => StarterPackScreen} options={{title: title(msg`Starter Pack`)}} /> <Stack.Screen name="StarterPackShort" getComponent={() => StarterPackScreenShort} options={{title: title(msg`Starter Pack`)}} /> <Stack.Screen name="StarterPackWizard" getComponent={() => Wizard} options={{title: title(msg`Create a starter pack`), requireAuth: true}} /> <Stack.Screen name="StarterPackEdit" getComponent={() => Wizard} options={{title: title(msg`Edit your starter pack`), requireAuth: true}} /> <Stack.Screen name="VideoFeed" getComponent={() => VideoFeed} options={{ title: title(msg`Video Feed`), requireAuth: true, }} /> <Stack.Screen name="Bookmarks" getComponent={() => BookmarksScreen} options={{ title: title(msg`Saved Posts`), requireAuth: true, }} /> </> )}
/** * The TabsNavigator is used by native mobile to represent the routes * in 3 distinct tab-stacks with a different root screen on each. */function TabsNavigator() { const tabBar = useCallback( (props: JSX.IntrinsicAttributes & BottomTabBarProps) => ( <BottomBar {...props} /> ), [], )
return ( <Tab.Navigator initialRouteName="HomeTab" backBehavior="initialRoute" screenOptions={{headerShown: false, lazy: true}} tabBar={tabBar}> <Tab.Screen name="HomeTab" getComponent={() => HomeTabNavigator} /> <Tab.Screen name="SearchTab" getComponent={() => SearchTabNavigator} /> <Tab.Screen name="MessagesTab" getComponent={() => MessagesTabNavigator} /> <Tab.Screen name="NotificationsTab" getComponent={() => NotificationsTabNavigator} /> <Tab.Screen name="MyProfileTab" getComponent={() => MyProfileTabNavigator} /> </Tab.Navigator> )}
function screenOptions(t: Theme) { return { fullScreenGestureEnabled: true, headerShown: false, contentStyle: t.atoms.bg, } as const}
function HomeTabNavigator() { const t = useTheme()
return ( <HomeTab.Navigator screenOptions={screenOptions(t)} initialRouteName="Home"> <HomeTab.Screen name="Home" getComponent={() => HomeScreen} /> <HomeTab.Screen name="Start" getComponent={() => HomeScreen} /> {commonScreens(HomeTab as typeof Flat)} </HomeTab.Navigator> )}
function SearchTabNavigator() { const t = useTheme() return ( <SearchTab.Navigator screenOptions={screenOptions(t)} initialRouteName="Search"> <SearchTab.Screen name="Search" getComponent={() => SearchScreen} /> {commonScreens(SearchTab as typeof Flat)} </SearchTab.Navigator> )}
function NotificationsTabNavigator() { const t = useTheme() return ( <NotificationsTab.Navigator screenOptions={screenOptions(t)} initialRouteName="Notifications"> <NotificationsTab.Screen name="Notifications" getComponent={() => NotificationsScreen} options={{requireAuth: true}} /> {commonScreens(NotificationsTab as typeof Flat)} </NotificationsTab.Navigator> )}
function MyProfileTabNavigator() { const t = useTheme() return ( <MyProfileTab.Navigator screenOptions={screenOptions(t)} initialRouteName="MyProfile"> <MyProfileTab.Screen // MyProfile is not in AllNavigationParams - asserting as Profile at least // gives us typechecking for initialParams -sfn name={'MyProfile' as 'Profile'} getComponent={() => ProfileScreen} initialParams={{name: 'me', hideBackButton: true}} /> {commonScreens(MyProfileTab as unknown as typeof Flat)} </MyProfileTab.Navigator> )}
function MessagesTabNavigator() { const t = useTheme() return ( <MessagesTab.Navigator screenOptions={screenOptions(t)} initialRouteName="Messages"> <MessagesTab.Screen name="Messages" getComponent={() => MessagesScreen} options={({route}) => ({ requireAuth: true, animationTypeForReplace: route.params?.animation ?? 'push', })} /> {commonScreens(MessagesTab as typeof Flat)} </MessagesTab.Navigator> )}
/** * The FlatNavigator is used by Web to represent the routes * in a single ("flat") stack. */const FlatNavigator = () => { const t = useTheme() const numUnread = useUnreadNotifications() const screenListeners = useWebScrollRestoration() const title = (page: MessageDescriptor) => bskyTitle(i18n._(page), numUnread)
return ( <Flat.Navigator screenListeners={screenListeners} screenOptions={screenOptions(t)}> <Flat.Screen name="Home" getComponent={() => HomeScreen} options={{title: title(msg`Home`)}} /> <Flat.Screen name="Search" getComponent={() => SearchScreen} options={{title: title(msg`Explore`)}} /> <Flat.Screen name="Notifications" getComponent={() => NotificationsScreen} options={{title: title(msg`Notifications`), requireAuth: true}} /> <Flat.Screen name="Messages" getComponent={() => MessagesScreen} options={{title: title(msg`Messages`), requireAuth: true}} /> <Flat.Screen name="Start" getComponent={() => HomeScreen} options={{title: title(msg`Home`)}} /> {commonScreens(Flat, numUnread)} </Flat.Navigator> )}
/** * The RoutesContainer should wrap all components which need access * to the navigation context. */
const LINKING = { // TODO figure out what we are going to use // note: `bluesky://` is what is used in app.config.js prefixes: ['bsky://', 'bluesky://', 'https://bsky.app'],
getPathFromState(state: State) { // find the current node in the navigation tree let node = state.routes[state.index || 0] while (node.state?.routes && typeof node.state?.index === 'number') { node = node.state?.routes[node.state?.index] }
// build the path const route = router.matchName(node.name) if (typeof route === 'undefined') { return '/' // default to home } return route.build((node.params || {}) as RouteParams) },
getStateFromPath(path: string) { const [name, params] = router.matchPath(path)
// Any time we receive a url that starts with `intent/` we want to ignore it here. It will be handled in the // intent handler hook. We should check for the trailing slash, because if there isn't one then it isn't a valid // intent // On web, there is no route state that's created by default, so we should initialize it as the home route. On // native, since the home tab and the home screen are defined as initial routes, we don't need to return a state // since it will be created by react-navigation. if (path.includes('intent/')) { if (isNative) return return buildStateObject('Flat', 'Home', params) }
if (isNative) { if (name === 'Search') { return buildStateObject('SearchTab', 'Search', params) } if (name === 'Notifications') { return buildStateObject('NotificationsTab', 'Notifications', params) } if (name === 'Home') { return buildStateObject('HomeTab', 'Home', params) } if (name === 'Messages') { return buildStateObject('MessagesTab', 'Messages', params) } // if the path is something else, like a post, profile, or even settings, we need to initialize the home tab as pre-existing state otherwise the back button will not work return buildStateObject('HomeTab', name, params, [ { name: 'Home', params: {}, }, ]) } else { const res = buildStateObject('Flat', name, params) return res } },} satisfies LinkingOptions<AllNavigatorParams>
/** * Used to ensure we don't handle the same notification twice */let lastHandledNotificationDateDedupe: number | undefined
function RoutesContainer({children}: React.PropsWithChildren<{}>) { const theme = useColorSchemeStyle(DefaultTheme, DarkTheme) const {currentAccount, accounts} = useSession() const {onPressSwitchAccount} = useAccountSwitcher() const {setShowLoggedOut} = useLoggedOutViewControls() const prevLoggedRouteName = useRef<string | undefined>(undefined) const emailDialogControl = useEmailDialogControl() const closeAllActiveElements = useCloseAllActiveElements()
/** * Handle navigation to a conversation, or prepares for account switch. * * Non-reactive because we need the latest data from some hooks * after an async call - sfn */ const handleChatMessage = useNonReactiveCallback( (payload: Extract<NotificationPayload, {reason: 'chat-message'}>) => { notyLogger.debug(`handleChatMessage`, {payload})
if (payload.recipientDid !== currentAccount?.did) { // handled in useNotificationHandler after account switch finishes storePayloadForAccountSwitch(payload) closeAllActiveElements()
const account = accounts.find(a => a.did === payload.recipientDid) if (account) { onPressSwitchAccount(account, 'Notification') } else { setShowLoggedOut(true) } } else { // @ts-expect-error nested navigators aren't typed -sfn navigate('MessagesTab', { screen: 'Messages', params: { pushToConversation: payload.convoId, }, }) } }, )
async function handlePushNotificationEntry() { if (!isNative) return
// deep links take precedence - on android, // getLastNotificationResponseAsync returns a "notification" // that is actually a deep link. avoid handling it twice -sfn if (await Linking.getInitialURL()) { return }
/** * The notification that caused the app to open, if applicable */ const response = await Notifications.getLastNotificationResponseAsync()
if (response) { notyLogger.debug(`handlePushNotificationEntry: response`, {response})
if (response.notification.date === lastHandledNotificationDateDedupe) return lastHandledNotificationDateDedupe = response.notification.date
const payload = getNotificationPayload(response.notification)
if (payload) { notyLogger.metric( 'notifications:openApp', {reason: payload.reason, causedBoot: true}, {statsig: false}, )
if (payload.reason === 'chat-message') { handleChatMessage(payload) } else { const path = notificationToURL(payload)
if (path === '/notifications') { resetToTab('NotificationsTab') notyLogger.debug(`handlePushNotificationEntry: default navigate`) } else if (path) { const [screen, params] = router.matchPath(path) // @ts-expect-error nested navigators aren't typed -sfn navigate('HomeTab', {screen, params}) notyLogger.debug(`handlePushNotificationEntry: navigate`, { screen, params, }) } } } } }
function onReady() { prevLoggedRouteName.current = getCurrentRouteName() if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) { emailDialogControl.open({ id: EmailDialogScreenID.VerificationReminder, }) snoozeEmailConfirmationPrompt() } }
return ( <> <NavigationContainer ref={navigationRef} linking={LINKING} theme={theme} onStateChange={() => { logger.metric( 'router:navigate', {from: prevLoggedRouteName.current}, {statsig: false}, ) prevLoggedRouteName.current = getCurrentRouteName() }} onReady={() => { attachRouteToLogEvents(getCurrentRouteName) logModuleInitTime() onReady() logger.metric('router:navigate', {}, {statsig: false}) handlePushNotificationEntry() }} // WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x // However, there's a fair amount of places we do that, especially in when popping to the top of stacks. // See BottomBar.tsx for an example of how to handle nested navigators in the tabs correctly. // I'm scared of missing a spot (esp. with push notifications etc) so let's enable this legacy behaviour for now. // We will need to confirm we handle nested navigators correctly by the time we migrate to React Navigation 8.x // -sfn navigationInChildEnabled> {children} </NavigationContainer> </> )}
function getCurrentRouteName() { if (navigationRef.isReady()) { return navigationRef.getCurrentRoute()?.name } else { return undefined }}
/** * These helpers can be used from outside of the RoutesContainer * (eg in the state models). */
function navigate<K extends keyof AllNavigatorParams>( name: K, params?: AllNavigatorParams[K],) { if (navigationRef.isReady()) { return Promise.race([ new Promise<void>(resolve => { const handler = () => { resolve() navigationRef.removeListener('state', handler) } navigationRef.addListener('state', handler)
// @ts-ignore I dont know what would make typescript happy but I have a life -prf navigationRef.navigate(name, params) }), timeout(1e3), ]) } return Promise.resolve()}
function resetToTab( tabName: 'HomeTab' | 'SearchTab' | 'MessagesTab' | 'NotificationsTab',) { if (navigationRef.isReady()) { navigate(tabName) if (navigationRef.canGoBack()) { navigationRef.dispatch(StackActions.popToTop()) //we need to check .canGoBack() before calling it } }}
// returns a promise that resolves after the state reset is completefunction reset(): Promise<void> { if (navigationRef.isReady()) { navigationRef.dispatch( CommonActions.reset({ index: 0, routes: [{name: isNative ? 'HomeTab' : 'Home'}], }), ) return Promise.race([ timeout(1e3), new Promise<void>(resolve => { const handler = () => { resolve() navigationRef.removeListener('state', handler) } navigationRef.addListener('state', handler) }), ]) } else { return Promise.resolve() }}
let didInit = falsefunction logModuleInitTime() { if (didInit) { return } didInit = true
const initMs = Math.round( // @ts-ignore Emitted by Metro in the bundle prelude performance.now() - global.__BUNDLE_START_TIME__, ) console.log(`Time to first paint: ${initMs} ms`) logEvent('init', { initMs, })
if (isWeb) { const referrerInfo = Referrer.getReferrerInfo() if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { logEvent('deepLink:referrerReceived', { to: window.location.href, referrer: referrerInfo?.referrer, hostname: referrerInfo?.hostname, }) } }
if (__DEV__) { // This log is noisy, so keep false committed const shouldLog = false // Relies on our patch to polyfill.js in metro-runtime const initLogs = (global as any).__INIT_LOGS__ if (shouldLog && Array.isArray(initLogs)) { console.log(initLogs.join('\n')) } }}
export { FlatNavigator, navigate, reset, resetToTab, RoutesContainer, TabsNavigator,}