Something went wrong. Try again.
Bluesky app fork with some witchin' additions 💫 forked from jollywhoppers.com/witchsky.app
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166import {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 {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'import { getNotificationPayload, type NotificationPayload, notificationToURL, storePayloadForAccountSwitch,} from '#/lib/hooks/useNotificationHandler'import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration'import {useCallOnce} from '#/lib/once'import {buildStateObject} from '#/lib/routes/helpers'import { type AllNavigatorParams, type BottomTabNavigatorParams, type FlatNavigatorParams, type HomeTabNavigatorParams, type MessagesTabNavigatorParams, type MyProfileTabNavigatorParams, type NotificationsTabNavigatorParams, type RouteParams, type SearchTabNavigatorParams, type State,} from '#/lib/routes/types'import {bskyTitle} from '#/lib/strings/headings'import {useDisableVerifyEmailReminder} from '#/state/preferences/disable-verify-email-reminder'import {useUnreadNotifications} from '#/state/queries/notifications/unread'import {useSession} from '#/state/session'import {useLoggedOutViewControls} from '#/state/shell/logged-out'import { shouldRequestEmailConfirmation, snoozeEmailConfirmationPrompt,} from '#/state/shell/reminders'import {useCloseAllActiveElements} from '#/state/util'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 {StorybookScreen} 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 {FindContactsFlowScreen} from '#/screens/FindContactsFlowScreen'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 {DeerSettingsScreen} from '#/screens/Settings/DeerSettings'import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences'import {FindContactsSettingsScreen} from '#/screens/Settings/FindContactsSettings'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 {useAnalytics} from '#/analytics'import {setNavigationMetadata} from '#/analytics/metadata'import {IS_NATIVE, IS_WEB} from '#/env'import {router} from '#/routes'import {Referrer} from '../modules/expo-bluesky-swiss-army'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
/** * Stores the navigation state before account switch on native. * This allows the user to stay on the same page when switching accounts. */let storedNavigationStateForAccountSwitch: State | undefined
export function storeNavigationStateForAccountSwitch() { if (IS_NATIVE && navigationRef.isReady()) { storedNavigationStateForAccountSwitch = navigationRef.getRootState() }}
function consumeStoredNavigationState(): State | undefined { const state = storedNavigationStateForAccountSwitch storedNavigationStateForAccountSwitch = undefined return state}
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={() => StorybookScreen} 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="DeerSettings" getComponent={() => DeerSettingsScreen} options={{ title: title(msg`Experiments`), 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="FindContactsSettings" getComponent={() => FindContactsSettingsScreen} options={{ title: title(msg`Find Contacts`), 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, }} /> <Stack.Screen name="FindContactsFlow" getComponent={() => FindContactsFlowScreen} options={{ title: title(msg`Find Contacts`), requireAuth: true, gestureEnabled: false, }} /> </> )}
/** * 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({ layout,}: { layout: React.ComponentProps<typeof Tab.Navigator>['layout']}) { const tabBar = useCallback( (props: JSX.IntrinsicAttributes & BottomTabBarProps) => ( <BottomBar {...props} /> ), [], )
return ( <Tab.Navigator initialRouteName="HomeTab" backBehavior="initialRoute" screenOptions={{headerShown: false, lazy: true}} tabBar={tabBar} layout={layout}> <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 = ({ layout,}: { layout: React.ComponentProps<typeof Flat.Navigator>['layout']}) => { const t = useTheme() const numUnread = useUnreadNotifications() const screenListeners = useWebScrollRestoration() const title = (page: MessageDescriptor) => bskyTitle(i18n._(page), numUnread)
return ( <Flat.Navigator layout={layout} 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://` and `witchsky://` is what is used in app.config.js prefixes: [ 'bsky://', 'bluesky://', 'witchsky://', 'https://bsky.app', 'https://witchsky.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 (IS_NATIVE) return return buildStateObject('Flat', 'Home', params) }
if (IS_NATIVE) { 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 ax = useAnalytics() const notyLogger = ax.logger.useChild(ax.logger.Context.Notifications) const theme = useColorSchemeStyle(DefaultTheme, DarkTheme) const {currentAccount, accounts} = useSession() const {onPressSwitchAccount} = useAccountSwitcher() const {setShowLoggedOut} = useLoggedOutViewControls() const previousScreen = useRef<string | undefined>(undefined) const emailDialogControl = useEmailDialogControl() const closeAllActiveElements = useCloseAllActiveElements()
const disableVerifyEmailReminder = useDisableVerifyEmailReminder()
// Consume stored navigation state from account switch (native only) const initialState = IS_NATIVE ? consumeStoredNavigationState() : undefined
/** * 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 (!IS_NATIVE) 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) { ax.metric('notifications:openApp', { reason: payload.reason, causedBoot: true, })
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, }) } } } } }
const onNavigationReady = useCallOnce(() => { const currentScreen = getCurrentRouteName() setNavigationMetadata({ previousScreen: currentScreen, currentScreen, }) previousScreen.current = currentScreen
handlePushNotificationEntry()
ax.metric('router:navigate', {})
if ( currentAccount && shouldRequestEmailConfirmation(currentAccount) && !disableVerifyEmailReminder ) { emailDialogControl.open({ id: EmailDialogScreenID.VerificationReminder, }) snoozeEmailConfirmationPrompt() }
ax.metric('init', { initMs: Math.round( // @ts-ignore Emitted by Metro in the bundle prelude performance.now() - global.__BUNDLE_START_TIME__, ), })
if (IS_WEB) { const referrerInfo = Referrer.getReferrerInfo() if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { ax.metric('deepLink:referrerReceived', { to: window.location.href, referrer: referrerInfo?.referrer, hostname: referrerInfo?.hostname, }) } } })
return ( <NavigationContainer ref={navigationRef} linking={LINKING} theme={theme} initialState={initialState} onStateChange={() => { const currentScreen = getCurrentRouteName() // do this before metric setNavigationMetadata({ previousScreen: previousScreen.current, currentScreen, }) ax.metric('router:navigate', {from: previousScreen.current}) previousScreen.current = currentScreen }} onReady={onNavigationReady} // 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: IS_NATIVE ? '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() }}
export { FlatNavigator, navigate, reset, resetToTab, RoutesContainer, TabsNavigator,}