From 9fdf987aa3777b3ea4b26c0ec6f181a38ff86c42 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 30 Dec 2025 20:23:53 +0200 Subject: [PATCH] use built-in documentTitle api --- src/Navigation.tsx | 212 +++++++++++++++++-------------- src/lib/hooks/useSetTitle.ts | 3 +- src/screens/PostThread/index.tsx | 11 +- 3 files changed, 128 insertions(+), 98 deletions(-) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index bf43eec6f1..083338db4a 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -1,8 +1,9 @@ -import {type JSX, useCallback, useRef} from 'react' +import {type JSX, useCallback, useMemo, useRef} from 'react' import {Linking} from 'react-native' import * as Notifications from 'expo-notifications' -import {i18n, type MessageDescriptor} from '@lingui/core' +import {type I18n} from '@lingui/core' import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import { type BottomTabBarProps, createBottomTabNavigator, @@ -42,10 +43,8 @@ import { } 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 {useLoggedOutViewControls} from '#/state/shell/logged-out' import { @@ -140,6 +139,8 @@ import { } from '#/components/dialogs/EmailDialog' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' +import {bskyTitle} from './lib/strings/headings' +import {useUnreadNotifications} from './state/queries/notifications/unread' const navigationRef = createNavigationContainerRef() @@ -157,47 +158,44 @@ const Tab = createBottomTabNavigator() /** * These "common screens" are reused across stacks. */ -function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { - const title = (page: MessageDescriptor) => - bskyTitle(i18n._(page), unreadCountLabel) - +function commonScreens(Stack: typeof Flat, i18n: I18n) { return ( <> NotFoundScreen} - options={{title: title(msg`Not Found`)}} + options={{title: i18n._(msg`Not Found`)}} /> ModerationScreen} - options={{title: title(msg`Moderation`), requireAuth: true}} + options={{title: i18n._(msg`Moderation`), requireAuth: true}} /> ModerationModlistsScreen} - options={{title: title(msg`Moderation Lists`), requireAuth: true}} + options={{title: i18n._(msg`Moderation Lists`), requireAuth: true}} /> ModerationMutedAccounts} - options={{title: title(msg`Muted Accounts`), requireAuth: true}} + options={{title: i18n._(msg`Muted Accounts`), requireAuth: true}} /> ModerationBlockedAccounts} - options={{title: title(msg`Blocked Accounts`), requireAuth: true}} + options={{title: i18n._(msg`Blocked Accounts`), requireAuth: true}} /> ModerationInteractionSettings} options={{ - title: title(msg`Post Interaction Settings`), + title: i18n._(msg`Post Interaction Settings`), requireAuth: true, }} /> @@ -205,176 +203,176 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="ModerationVerificationSettings" getComponent={() => ModerationVerificationSettings} options={{ - title: title(msg`Verification Settings`), + title: i18n._(msg`Verification Settings`), requireAuth: true, }} /> SettingsScreen} - options={{title: title(msg`Settings`), requireAuth: true}} + options={{title: i18n._(msg`Settings`), requireAuth: true}} /> LanguageSettingsScreen} - options={{title: title(msg`Language Settings`), requireAuth: true}} + options={{title: i18n._(msg`Language Settings`), requireAuth: true}} /> ProfileScreen} options={({route}) => ({ - title: bskyTitle(`@${route.params.name}`, unreadCountLabel), + title: `@${route.params.name}`, })} /> ProfileFollowersScreen} options={({route}) => ({ - title: title(msg`People following @${route.params.name}`), + title: i18n._(msg`People following @${route.params.name}`), })} /> ProfileFollowsScreen} options={({route}) => ({ - title: title(msg`People followed by @${route.params.name}`), + title: i18n._(msg`People followed by @${route.params.name}`), })} /> ProfileKnownFollowersScreen} options={({route}) => ({ - title: title(msg`Followers of @${route.params.name} that you know`), + title: i18n._(msg`Followers of @${route.params.name} that you know`), })} /> ProfileListScreen} - options={{title: title(msg`List`), requireAuth: true}} + options={{title: i18n._(msg`List`), requireAuth: true}} /> ProfileSearchScreen} options={({route}) => ({ - title: title(msg`Search @${route.params.name}'s posts`), + title: i18n._(msg`Search @${route.params.name}'s posts`), })} /> PostThreadScreen} options={({route}) => ({ - title: title(msg`Post by @${route.params.name}`), + title: i18n._(msg`Post by @${route.params.name}`), })} /> PostLikedByScreen} options={({route}) => ({ - title: title(msg`Post by @${route.params.name}`), + title: i18n._(msg`Post by @${route.params.name}`), })} /> PostRepostedByScreen} options={({route}) => ({ - title: title(msg`Post by @${route.params.name}`), + title: i18n._(msg`Post by @${route.params.name}`), })} /> PostQuotesScreen} options={({route}) => ({ - title: title(msg`Post by @${route.params.name}`), + title: i18n._(msg`Post by @${route.params.name}`), })} /> ProfileFeedScreen} - options={{title: title(msg`Feed`)}} + options={{title: i18n._(msg`Feed`)}} /> ProfileFeedLikedByScreen} - options={{title: title(msg`Liked by`)}} + options={{title: i18n._(msg`Liked by`)}} /> ProfileLabelerLikedByScreen} - options={{title: title(msg`Liked by`)}} + options={{title: i18n._(msg`Liked by`)}} /> Storybook} - options={{title: title(msg`Storybook`), requireAuth: true}} + options={{title: i18n._(msg`Storybook`), requireAuth: true}} /> DebugModScreen} - options={{title: title(msg`Moderation states`), requireAuth: true}} + options={{title: i18n._(msg`Moderation states`), requireAuth: true}} /> SharedPreferencesTesterScreen} - options={{title: title(msg`Shared Preferences Tester`)}} + options={{title: i18n._(msg`Shared Preferences Tester`)}} /> LogScreen} - options={{title: title(msg`Log`), requireAuth: true}} + options={{title: i18n._(msg`Log`), requireAuth: true}} /> SupportScreen} - options={{title: title(msg`Support`)}} + options={{title: i18n._(msg`Support`)}} /> PrivacyPolicyScreen} - options={{title: title(msg`Privacy Policy`)}} + options={{title: i18n._(msg`Privacy Policy`)}} /> TermsOfServiceScreen} - options={{title: title(msg`Terms of Service`)}} + options={{title: i18n._(msg`Terms of Service`)}} /> CommunityGuidelinesScreen} - options={{title: title(msg`Community Guidelines`)}} + options={{title: i18n._(msg`Community Guidelines`)}} /> CopyrightPolicyScreen} - options={{title: title(msg`Copyright Policy`)}} + options={{title: i18n._(msg`Copyright Policy`)}} /> AppPasswordsScreen} - options={{title: title(msg`App Passwords`), requireAuth: true}} + options={{title: i18n._(msg`App Passwords`), requireAuth: true}} /> SavedFeeds} - options={{title: title(msg`Edit My Feeds`), requireAuth: true}} + options={{title: i18n._(msg`Edit My Feeds`), requireAuth: true}} /> FollowingFeedPreferencesScreen} options={{ - title: title(msg`Following Feed Preferences`), + title: i18n._(msg`Following Feed Preferences`), requireAuth: true, }} /> ThreadPreferencesScreen} - options={{title: title(msg`Threads Preferences`), requireAuth: true}} + options={{title: i18n._(msg`Threads Preferences`), requireAuth: true}} /> ExternalMediaPreferencesScreen} options={{ - title: title(msg`External Media Preferences`), + title: i18n._(msg`External Media Preferences`), requireAuth: true, }} /> @@ -382,7 +380,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="AccessibilitySettings" getComponent={() => AccessibilitySettingsScreen} options={{ - title: title(msg`Accessibility Settings`), + title: i18n._(msg`Accessibility Settings`), requireAuth: true, }} /> @@ -390,7 +388,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="AppearanceSettings" getComponent={() => AppearanceSettingsScreen} options={{ - title: title(msg`Appearance`), + title: i18n._(msg`Appearance`), requireAuth: true, }} /> @@ -398,7 +396,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="AccountSettings" getComponent={() => AccountSettingsScreen} options={{ - title: title(msg`Account`), + title: i18n._(msg`Account`), requireAuth: true, }} /> @@ -406,7 +404,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="PrivacyAndSecuritySettings" getComponent={() => PrivacyAndSecuritySettingsScreen} options={{ - title: title(msg`Privacy and Security`), + title: i18n._(msg`Privacy and Security`), requireAuth: true, }} /> @@ -414,7 +412,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="ActivityPrivacySettings" getComponent={() => ActivityPrivacySettingsScreen} options={{ - title: title(msg`Privacy and Security`), + title: i18n._(msg`Privacy and Security`), requireAuth: true, }} /> @@ -422,20 +420,20 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="FindContactsSettings" getComponent={() => FindContactsSettingsScreen} options={{ - title: title(msg`Find Contacts`), + title: i18n._(msg`Find Contacts`), requireAuth: true, }} /> NotificationSettingsScreen} - options={{title: title(msg`Notification settings`), requireAuth: true}} + options={{title: i18n._(msg`Notification settings`), requireAuth: true}} /> ReplyNotificationSettingsScreen} options={{ - title: title(msg`Reply notifications`), + title: i18n._(msg`Reply notifications`), requireAuth: true, }} /> @@ -443,7 +441,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="MentionNotificationSettings" getComponent={() => MentionNotificationSettingsScreen} options={{ - title: title(msg`Mention notifications`), + title: i18n._(msg`Mention notifications`), requireAuth: true, }} /> @@ -451,7 +449,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="QuoteNotificationSettings" getComponent={() => QuoteNotificationSettingsScreen} options={{ - title: title(msg`Quote notifications`), + title: i18n._(msg`Quote notifications`), requireAuth: true, }} /> @@ -459,7 +457,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="LikeNotificationSettings" getComponent={() => LikeNotificationSettingsScreen} options={{ - title: title(msg`Like notifications`), + title: i18n._(msg`Like notifications`), requireAuth: true, }} /> @@ -467,7 +465,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="RepostNotificationSettings" getComponent={() => RepostNotificationSettingsScreen} options={{ - title: title(msg`Repost notifications`), + title: i18n._(msg`Repost notifications`), requireAuth: true, }} /> @@ -475,7 +473,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="NewFollowerNotificationSettings" getComponent={() => NewFollowerNotificationSettingsScreen} options={{ - title: title(msg`New follower notifications`), + title: i18n._(msg`New follower notifications`), requireAuth: true, }} /> @@ -483,7 +481,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="LikesOnRepostsNotificationSettings" getComponent={() => LikesOnRepostsNotificationSettingsScreen} options={{ - title: title(msg`Likes of your reposts notifications`), + title: i18n._(msg`Likes of your reposts notifications`), requireAuth: true, }} /> @@ -491,7 +489,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="RepostsOnRepostsNotificationSettings" getComponent={() => RepostsOnRepostsNotificationSettingsScreen} options={{ - title: title(msg`Reposts of your reposts notifications`), + title: i18n._(msg`Reposts of your reposts notifications`), requireAuth: true, }} /> @@ -499,7 +497,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="ActivityNotificationSettings" getComponent={() => ActivityNotificationSettingsScreen} options={{ - title: title(msg`Activity notifications`), + title: i18n._(msg`Activity notifications`), requireAuth: true, }} /> @@ -507,7 +505,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="MiscellaneousNotificationSettings" getComponent={() => MiscellaneousNotificationSettingsScreen} options={{ - title: title(msg`Miscellaneous notifications`), + title: i18n._(msg`Miscellaneous notifications`), requireAuth: true, }} /> @@ -515,7 +513,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="ContentAndMediaSettings" getComponent={() => ContentAndMediaSettingsScreen} options={{ - title: title(msg`Content and Media`), + title: i18n._(msg`Content and Media`), requireAuth: true, }} /> @@ -523,7 +521,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="InterestsSettings" getComponent={() => InterestsSettingsScreen} options={{ - title: title(msg`Your interests`), + title: i18n._(msg`Your interests`), requireAuth: true, }} /> @@ -531,7 +529,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="AboutSettings" getComponent={() => AboutSettingsScreen} options={{ - title: title(msg`About`), + title: i18n._(msg`About`), requireAuth: true, }} /> @@ -539,75 +537,78 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="AppIconSettings" getComponent={() => AppIconSettingsScreen} options={{ - title: title(msg`App Icon`), + title: i18n._(msg`App Icon`), requireAuth: true, }} /> HashtagScreen} - options={{title: title(msg`Hashtag`)}} + options={{title: i18n._(msg`Hashtag`)}} /> TopicScreen} - options={{title: title(msg`Topic`)}} + options={{title: i18n._(msg`Topic`)}} /> MessagesConversationScreen} - options={{title: title(msg`Chat`), requireAuth: true}} + options={{title: i18n._(msg`Chat`), requireAuth: true}} /> MessagesSettingsScreen} - options={{title: title(msg`Chat settings`), requireAuth: true}} + options={{title: i18n._(msg`Chat settings`), requireAuth: true}} /> MessagesInboxScreen} - options={{title: title(msg`Chat request inbox`), requireAuth: true}} + options={{title: i18n._(msg`Chat request inbox`), requireAuth: true}} /> NotificationsActivityListScreen} - options={{title: title(msg`Notifications`), requireAuth: true}} + options={{title: i18n._(msg`Notifications`), requireAuth: true}} /> LegacyNotificationSettingsScreen} - options={{title: title(msg`Notification settings`), requireAuth: true}} + options={{title: i18n._(msg`Notification settings`), requireAuth: true}} /> FeedsScreen} - options={{title: title(msg`Feeds`)}} + options={{title: i18n._(msg`Feeds`)}} /> StarterPackScreen} - options={{title: title(msg`Starter Pack`)}} + options={{title: i18n._(msg`Starter Pack`)}} /> StarterPackScreenShort} - options={{title: title(msg`Starter Pack`)}} + options={{title: i18n._(msg`Starter Pack`)}} /> Wizard} - options={{title: title(msg`Create a starter pack`), requireAuth: true}} + options={{title: i18n._(msg`Create a starter pack`), requireAuth: true}} /> Wizard} - options={{title: title(msg`Edit your starter pack`), requireAuth: true}} + options={{ + title: i18n._(msg`Edit your starter pack`), + requireAuth: true, + }} /> VideoFeed} options={{ - title: title(msg`Video Feed`), + title: i18n._(msg`Video Feed`), requireAuth: true, }} /> @@ -615,7 +616,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="Bookmarks" getComponent={() => BookmarksScreen} options={{ - title: title(msg`Saved Posts`), + title: i18n._(msg`Saved Posts`), requireAuth: true, }} /> @@ -623,7 +624,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { name="FindContactsFlow" getComponent={() => FindContactsFlowScreen} options={{ - title: title(msg`Find Contacts`), + title: i18n._(msg`Find Contacts`), requireAuth: true, gestureEnabled: false, }} @@ -683,30 +684,35 @@ function screenOptions(t: Theme) { function HomeTabNavigator() { const t = useTheme() + const {i18n} = useLingui() return ( HomeScreen} /> HomeScreen} /> - {commonScreens(HomeTab as typeof Flat)} + {commonScreens(HomeTab as typeof Flat, i18n)} ) } function SearchTabNavigator() { const t = useTheme() + const {i18n} = useLingui() + return ( SearchScreen} /> - {commonScreens(SearchTab as typeof Flat)} + {commonScreens(SearchTab as typeof Flat, i18n)} ) } function NotificationsTabNavigator() { const t = useTheme() + const {i18n} = useLingui() + return ( NotificationsScreen} options={{requireAuth: true}} /> - {commonScreens(NotificationsTab as typeof Flat)} + {commonScreens(NotificationsTab as typeof Flat, i18n)} ) } function MyProfileTabNavigator() { const t = useTheme() + const {i18n} = useLingui() + return ( ProfileScreen} initialParams={{name: 'me', hideBackButton: true}} /> - {commonScreens(MyProfileTab as unknown as typeof Flat)} + {commonScreens(MyProfileTab as unknown as typeof Flat, i18n)} ) } function MessagesTabNavigator() { const t = useTheme() + const {i18n} = useLingui() + return ( - {commonScreens(MessagesTab as typeof Flat)} + {commonScreens(MessagesTab as typeof Flat, i18n)} ) } @@ -768,9 +778,8 @@ const FlatNavigator = ({ layout: React.ComponentProps['layout'] }) => { const t = useTheme() - const numUnread = useUnreadNotifications() + const {_, i18n} = useLingui() const screenListeners = useWebScrollRestoration() - const title = (page: MessageDescriptor) => bskyTitle(i18n._(page), numUnread) return ( HomeScreen} - options={{title: title(msg`Home`)}} + options={{title: _(msg`Home`)}} /> SearchScreen} - options={{title: title(msg`Explore`)}} + options={{title: _(msg`Explore`)}} /> NotificationsScreen} - options={{title: title(msg`Notifications`), requireAuth: true}} + options={{title: _(msg`Notifications`), requireAuth: true}} /> MessagesScreen} - options={{title: title(msg`Messages`), requireAuth: true}} + options={{title: _(msg`Messages`), requireAuth: true}} /> HomeScreen} - options={{title: title(msg`Home`)}} + options={{title: _(msg`Home`)}} /> - {commonScreens(Flat, numUnread)} + {commonScreens(Flat, i18n)} ) } @@ -880,6 +889,7 @@ let lastHandledNotificationDateDedupe: number | undefined function RoutesContainer({children}: React.PropsWithChildren<{}>) { const theme = useColorSchemeStyle(DefaultTheme, DarkTheme) + const unreadCount = useUnreadNotifications() const {currentAccount, accounts} = useSession() const {onPressSwitchAccount} = useAccountSwitcher() const {setShowLoggedOut} = useLoggedOutViewControls() @@ -983,6 +993,17 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { } } + const documentTitle = useMemo(() => { + return { + formatter: (page: Record | undefined) => { + if (page?.title) { + return bskyTitle(page.title, unreadCount) + } + return 'Bluesky' + }, + } + }, [unreadCount]) + return ( <> ) { // 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> + navigationInChildEnabled + documentTitle={documentTitle}> {children} diff --git a/src/lib/hooks/useSetTitle.ts b/src/lib/hooks/useSetTitle.ts index 04f9dcc2c3..a85e5bfde3 100644 --- a/src/lib/hooks/useSetTitle.ts +++ b/src/lib/hooks/useSetTitle.ts @@ -2,7 +2,6 @@ import {useEffect} from 'react' import {useNavigation} from '@react-navigation/native' import {type NavigationProp} from '#/lib/routes/types' -import {bskyTitle} from '#/lib/strings/headings' import {useUnreadNotifications} from '#/state/queries/notifications/unread' export function useSetTitle(title?: string) { @@ -10,7 +9,7 @@ export function useSetTitle(title?: string) { const numUnread = useUnreadNotifications() useEffect(() => { if (title) { - navigation.setOptions({title: bskyTitle(title, numUnread)}) + navigation.setOptions({title}) } }, [title, navigation, numUnread]) } diff --git a/src/screens/PostThread/index.tsx b/src/screens/PostThread/index.tsx index 7e130f71f6..5bbb00c36f 100644 --- a/src/screens/PostThread/index.tsx +++ b/src/screens/PostThread/index.tsx @@ -1,11 +1,13 @@ import {useCallback, useEffect, useMemo, useRef, useState} from 'react' import {useWindowDimensions, View} from 'react-native' import Animated, {useAnimatedStyle} from 'react-native-reanimated' -import {Trans} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' +import {useSetTitle} from '#/lib/hooks/useSetTitle' import {logger} from '#/logger' import {useFeedFeedback} from '#/state/feed-feedback' import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences' @@ -49,6 +51,7 @@ const PARENT_CHUNK_SIZE = 5 const CHILDREN_CHUNK_SIZE = 50 export function PostThread({uri}: {uri: string}) { + const {_} = useLingui() const {gtMobile} = useBreakpoints() const {hasSession} = useSession() const initialNumToRender = useInitialNumToRender() @@ -75,6 +78,12 @@ export function PostThread({uri}: {uri: string}) { return {hasParents} }, [thread.data.items]) + useSetTitle( + anchor?.value.post + ? _(msg`Post by @${anchor.value.post.author.handle}`) + : undefined, + ) + // Track post:view event when anchor post is viewed const seenPostUriRef = useRef(null) useEffect(() => {