Something went wrong. Try again.
Bluesky app fork with some witchin' additions 💫 forked from jollywhoppers.com/witchsky.app
Something went wrong. Try again.
45 kB · 1305 lines
TSX
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306import {type JSX, useCallback, useRef, useState} from 'react'import * as Linking from 'expo-linking'import * as Notifications from 'expo-notifications'import {i18n, type MessageDescriptor} from '@lingui/core'import {msg} from '@lingui/core/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 {AuthCallback} from '#/screens/Login/AuthCallback'import {MessagesScreen} from '#/screens/Messages/ChatList'import {MessagesConversationScreen} from '#/screens/Messages/Conversation'import {MessagesConversationSettingsScreen} from '#/screens/Messages/ConversationSettings'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 {AIPreferencesSettingsScreen} from '#/screens/Settings/AIPreferencesSettings'import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'import {AppearanceColorThemeSettingsScreen} from '#/screens/Settings/AppearanceSettings/ColorThemeSettings'import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords'import {AutomationLabelSettingsScreen} from '#/screens/Settings/AutomationLabelSettings'import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings'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 {PetLabelSettingsScreen} from '#/screens/Settings/PetLabelSettings'import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings'import {RunesSettingsScreen} from '#/screens/Settings/RunesSettings'import {RunesDisplayAlsoLikedSettingsScreen} from '#/screens/Settings/RunesSettings/AlsoLikedSettings'import {RunesBadgesSettingsScreen} from '#/screens/Settings/RunesSettings/BadgesSettings'import {RunesDisplayDensitySettingsScreen} from '#/screens/Settings/RunesSettings/DensitySettings'import {RunesDisplaySettingsScreen} from '#/screens/Settings/RunesSettings/DisplaySettings'import {RunesExtraSettingsScreen} from '#/screens/Settings/RunesSettings/ExtraSettings'import {RunesUsabilityFeedSettingsScreen} from '#/screens/Settings/RunesSettings/FeedSettings'import {RunesImpressionsSettingsScreen} from '#/screens/Settings/RunesSettings/ImpressionsSettings'import {RunesInfrastructureSettingsScreen} from '#/screens/Settings/RunesSettings/InfrastructureSettings'import {RunesMenusSettingsScreen} from '#/screens/Settings/RunesSettings/MenusSettings'import {RunesSettingsSyncSettingsScreen} from '#/screens/Settings/RunesSettings/SettingsSyncSettings'import {RunesUsabilitySettingsScreen} from '#/screens/Settings/RunesSettings/UsabilitySettings'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_LIQUID_GLASS, 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="AuthCallback" getComponent={() => AuthCallback} options={{title: title(msg`Signing in...`)}} /> <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="AIPreferencesSettings" getComponent={() => AIPreferencesSettingsScreen} options={{ title: title(msg`AI Preferences`), requireAuth: true, }} /> <Stack.Screen name="RunesSettings" getComponent={() => RunesSettingsScreen} options={{ title: title(msg`Runes`), requireAuth: true, }} /> <Stack.Screen name="RunesMenusSettings" getComponent={() => RunesMenusSettingsScreen} options={{title: title(msg`Menus`), requireAuth: true}} /> <Stack.Screen name="RunesBadgesSettings" getComponent={() => RunesBadgesSettingsScreen} options={{title: title(msg`Badges`), requireAuth: true}} /> <Stack.Screen name="RunesImpressionsSettings" getComponent={() => RunesImpressionsSettingsScreen} options={{title: title(msg`Impressions`), requireAuth: true}} /> <Stack.Screen name="RunesUsabilitySettings" getComponent={() => RunesUsabilitySettingsScreen} options={{title: title(msg`Usability`), requireAuth: true}} /> <Stack.Screen name="RunesUsabilityFeedSettings" getComponent={() => RunesUsabilityFeedSettingsScreen} options={{title: title(msg`Feeds`), requireAuth: true}} /> <Stack.Screen name="RunesDisplaySettings" getComponent={() => RunesDisplaySettingsScreen} options={{title: title(msg`Display`), requireAuth: true}} /> <Stack.Screen name="RunesDisplayAlsoLikedSettings" getComponent={() => RunesDisplayAlsoLikedSettingsScreen} options={{title: title(msg`Also liked`), requireAuth: true}} /> <Stack.Screen name="RunesDisplayDensitySettings" getComponent={() => RunesDisplayDensitySettingsScreen} options={{title: title(msg`Density`), requireAuth: true}} /> <Stack.Screen name="RunesInfrastructureSettings" getComponent={() => RunesInfrastructureSettingsScreen} options={{title: title(msg`Infrastructure`), requireAuth: true}} /> <Stack.Screen name="RunesExtraSettings" getComponent={() => RunesExtraSettingsScreen} options={{title: title(msg`Extra settings`), requireAuth: true}} /> <Stack.Screen name="RunesSettingsSyncSettings" getComponent={() => RunesSettingsSyncSettingsScreen} options={{title: title(msg`Settings sync`), requireAuth: true}} /> <Stack.Screen name="AppearanceSettings" getComponent={() => AppearanceSettingsScreen} options={{ title: title(msg`Appearance`), requireAuth: true, }} /> <Stack.Screen name="AppearanceColorThemeSettings" getComponent={() => AppearanceColorThemeSettingsScreen} options={{ title: title(msg`Color Theme`), requireAuth: true, }} /> <Stack.Screen name="AccountSettings" getComponent={() => AccountSettingsScreen} options={{ title: title(msg`Account`), requireAuth: true, }} /> <Stack.Screen name="AutomationLabelSettings" getComponent={() => AutomationLabelSettingsScreen} options={{ title: title(msg`Automation Label`), requireAuth: true, }} /> <Stack.Screen name="PetLabelSettings" getComponent={() => PetLabelSettingsScreen} options={{ title: title(msg`Pet Label`), 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="MessagesConversationSettings" getComponent={() => MessagesConversationSettingsScreen} options={{title: title(msg`Group chat settings`), 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()
const BLURRED_SCROLL_EDGE_EFFECT = IS_LIQUID_GLASS ? ({ headerShown: true, headerTransparent: true, headerTitle: '', headerBackVisible: false, scrollEdgeEffects: { top: 'soft', }, } as const) : {}
return ( <HomeTab.Navigator screenOptions={screenOptions(t)} initialRouteName="Home"> <HomeTab.Screen name="Home" getComponent={() => HomeScreen} options={BLURRED_SCROLL_EDGE_EFFECT} /> <HomeTab.Screen name="Start" getComponent={() => HomeScreen} options={BLURRED_SCROLL_EDGE_EFFECT} /> {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>
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 linkingUrl = Linking.useLinkingURL()
const disableVerifyEmailReminder = useDisableVerifyEmailReminder()
// Consume stored navigation state exactly once for this mount. // Account switches trigger session updates before the keyed shell subtree // remounts, so consuming during render can clear the stored back stack too // early and leave React Navigation to reconstruct only the active route. const [initialState, setInitialState] = useState(() => IS_NATIVE ? consumeStoredNavigationState() : undefined, ) void setInitialState
/** * 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, }, }) } }, )
function handlePushNotificationEntry() { if (!IS_NATIVE) return
// intent urls are handled by `useIntentHandler` if (linkingUrl) return
const notificationResponse = Notifications.getLastNotificationResponse()
if (notificationResponse) { notyLogger.debug(`handlePushNotificationEntry: response`, { response: notificationResponse, })
// Clear the last notification response to ensure it's not used again try { Notifications.clearLastNotificationResponse() } catch (error) { notyLogger.error( `handlePushNotificationEntry: error clearing notification response`, {error}, ) }
const payload = getNotificationPayload(notificationResponse.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, }) } }
// temp, just testing void ax.features.enabled(ax.features.AATest) })
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,}