diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 26a2b2a2ae..5ea3b1a2b6 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -123,6 +123,7 @@ import {QuoteNotificationSettingsScreen} from './screens/Settings/NotificationSe import {ReplyNotificationSettingsScreen} from './screens/Settings/NotificationSettings/ReplyNotificationSettings' import {RepostNotificationSettingsScreen} from './screens/Settings/NotificationSettings/RepostNotificationSettings' import {RepostsOnRepostsNotificationSettingsScreen} from './screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings' +import {NavigationAvailable} from './state/shell/navigation-available' const navigationRef = createNavigationContainerRef() @@ -825,6 +826,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { const {currentAccount} = useSession() const prevLoggedRouteName = React.useRef(undefined) const emailDialogControl = useEmailDialogControl() + const navigationAvailableRef = React.useRef<{onReady: () => void}>(null) function onReady() { prevLoggedRouteName.current = getCurrentRouteName() @@ -837,7 +839,9 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { } return ( - <> + ) { attachRouteToLogEvents(getCurrentRouteName) logModuleInitTime() onReady() + navigationAvailableRef.current?.onReady() logger.metric('router:navigate', {}, {statsig: false}) }} // WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x @@ -867,7 +872,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { navigationInChildEnabled> {children} - + ) } diff --git a/src/lib/hooks/useNotificationHandler.ts b/src/lib/hooks/useNotificationHandler.ts index 6c3e7deb82..6d07ef61ad 100644 --- a/src/lib/hooks/useNotificationHandler.ts +++ b/src/lib/hooks/useNotificationHandler.ts @@ -16,6 +16,7 @@ import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread' import {truncateAndInvalidate} from '#/state/queries/util' import {useSession} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' +import {useRunWhenNavigationAvailable} from '#/state/shell/navigation-available' import {useCloseAllActiveElements} from '#/state/util' import {resetToTab} from '#/Navigation' @@ -74,6 +75,7 @@ export function useNotificationsHandler() { const {currentConvoId} = useCurrentConvoId() const {setShowLoggedOut} = useLoggedOutViewControls() const closeAllActiveElements = useCloseAllActiveElements() + const runWhenNavigationAvailable = useRunWhenNavigationAvailable() const {_} = useLingui() // On Android, we cannot control which sound is used for a notification on Android @@ -228,10 +230,17 @@ export function useNotificationsHandler() { } } else { switch (payload.reason) { + case 'like': + case 'repost': + case 'reply': + case 'quote': + case 'mention': + case 'like-via-repost': + case 'repost-via-repost': case 'subscribed-post': - const urip = new AtUri(payload.uri) - if (urip.collection === 'app.bsky.feed.post') { - setTimeout(() => { + runWhenNavigationAvailable(() => { + const urip = new AtUri(payload.uri) + if (urip.collection === 'app.bsky.feed.post') { // @ts-expect-error types are weird here navigation.navigate('HomeTab', { screen: 'PostThread', @@ -240,51 +249,29 @@ export function useNotificationsHandler() { rkey: urip.rkey, }, }) - }, 500) - } else { - resetToTab('NotificationsTab') - } + } else { + resetToTab('NotificationsTab') + } + }) break - case 'like': - case 'repost': case 'follow': - case 'mention': - case 'quote': - case 'reply': case 'starterpack-joined': - case 'like-via-repost': - case 'repost-via-repost': + runWhenNavigationAvailable(() => { + const urip = new AtUri(payload.uri) + // @ts-expect-error types are weird here + navigation.navigate('HomeTab', { + screen: 'Profile', + params: { + name: urip.host, + }, + }) + }) + break case 'verified': case 'unverified': default: resetToTab('NotificationsTab') break - // TODO implement these after we have an idea of how to handle each individual case - // case 'follow': - // const uri = new AtUri(payload.uri) - // setTimeout(() => { - // // @ts-expect-error types are weird here - // navigation.navigate('HomeTab', { - // screen: 'Profile', - // params: { - // name: uri.host, - // }, - // }) - // }, 500) - // break - // case 'mention': - // case 'reply': - // const urip = new AtUri(payload.uri) - // setTimeout(() => { - // // @ts-expect-error types are weird here - // navigation.navigate('HomeTab', { - // screen: 'PostThread', - // params: { - // name: urip.host, - // rkey: urip.rkey, - // }, - // }) - // }, 500) } } } @@ -416,5 +403,6 @@ export function useNotificationsHandler() { navigation, onPressSwitchAccount, setShowLoggedOut, + runWhenNavigationAvailable, ]) } diff --git a/src/state/shell/navigation-available.tsx b/src/state/shell/navigation-available.tsx new file mode 100644 index 0000000000..3464f49add --- /dev/null +++ b/src/state/shell/navigation-available.tsx @@ -0,0 +1,57 @@ +import {createContext, useContext, useImperativeHandle, useState} from 'react' +import {type NavigationContainerRefWithCurrent} from '@react-navigation/native' + +import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' +import {type AllNavigatorParams} from '#/lib/routes/types' + +type QueueAction = () => void +const NavigationAvailableContext = createContext< + ((cb: QueueAction) => void) | null +>(null) + +export function NavigationAvailable({ + children, + ref, + navigationRef, +}: { + children: React.ReactNode + ref: React.Ref<{onReady: () => void}> + navigationRef: NavigationContainerRefWithCurrent +}) { + const [queue, setQueue] = useState([]) + + useImperativeHandle(ref, () => ({ + onReady: () => { + for (const item of queue) { + item() + } + setQueue([]) + }, + })) + + const runWhenNavigationAvailable = useNonReactiveCallback( + (cb: QueueAction) => { + if (navigationRef.isReady()) { + cb() + } else { + setQueue(prev => [...prev, cb]) + } + }, + ) + + return ( + + {children} + + ) +} + +export function useRunWhenNavigationAvailable() { + const context = useContext(NavigationAvailableContext) + if (!context) { + throw new Error( + 'useRunWhenNavigationAvailable must be used within a NavigationAvailable component', + ) + } + return context +}