diff options
-rw-r--r-- | src/Navigation.tsx | 133 | ||||
-rw-r--r-- | src/lib/hooks/useSetTitle.ts | 16 | ||||
-rw-r--r-- | src/lib/hooks/useUnreadCountLabel.ts | 19 | ||||
-rw-r--r-- | src/lib/strings/display-names.ts | 15 | ||||
-rw-r--r-- | src/lib/strings/headings.ts | 4 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThread.tsx | 9 | ||||
-rw-r--r-- | src/view/screens/Profile.tsx | 3 | ||||
-rw-r--r-- | src/view/screens/ProfileList.tsx | 2 |
8 files changed, 180 insertions, 21 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 4e0403be9..17bb3c159 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -28,6 +28,7 @@ import {isNative} from 'platform/detection' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {router} from './routes' import {usePalette} from 'lib/hooks/usePalette' +import {useUnreadCountLabel} from 'lib/hooks/useUnreadCountLabel' import {useStores} from './state' import {HomeScreen} from './view/screens/Home' @@ -55,6 +56,7 @@ import {AppPasswords} from 'view/screens/AppPasswords' import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts' import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts' import {getRoutingInstrumentation} from 'lib/sentry' +import {bskyTitle} from 'lib/strings/headings' const navigationRef = createNavigationContainerRef<AllNavigatorParams>() @@ -69,45 +71,120 @@ const Tab = createBottomTabNavigator<BottomTabNavigatorParams>() /** * These "common screens" are reused across stacks. */ -function commonScreens(Stack: typeof HomeTab) { +function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { + const title = (page: string) => bskyTitle(page, unreadCountLabel) + return ( <> - <Stack.Screen name="NotFound" component={NotFoundScreen} /> - <Stack.Screen name="Moderation" component={ModerationScreen} /> + <Stack.Screen + name="NotFound" + component={NotFoundScreen} + options={{title: title('Not Found')}} + /> + <Stack.Screen + name="Moderation" + component={ModerationScreen} + options={{title: title('Moderation')}} + /> <Stack.Screen name="ModerationMuteLists" component={ModerationMuteListsScreen} + options={{title: title('Mute Lists')}} /> <Stack.Screen name="ModerationMutedAccounts" component={ModerationMutedAccounts} + options={{title: title('Muted Accounts')}} /> <Stack.Screen name="ModerationBlockedAccounts" component={ModerationBlockedAccounts} + options={{title: title('Blocked Accounts')}} + /> + <Stack.Screen + name="Settings" + component={SettingsScreen} + options={{title: title('Settings')}} + /> + <Stack.Screen + name="Profile" + component={ProfileScreen} + options={({route}) => ({title: title(`@${route.params.name}`)})} /> - <Stack.Screen name="Settings" component={SettingsScreen} /> - <Stack.Screen name="Profile" component={ProfileScreen} /> <Stack.Screen name="ProfileFollowers" component={ProfileFollowersScreen} + options={({route}) => ({ + title: title(`People following @${route.params.name}`), + })} + /> + <Stack.Screen + name="ProfileFollows" + component={ProfileFollowsScreen} + options={({route}) => ({ + title: title(`People followed by @${route.params.name}`), + })} + /> + <Stack.Screen + name="ProfileList" + component={ProfileListScreen} + options={{title: title('Mute List')}} + /> + <Stack.Screen + name="PostThread" + component={PostThreadScreen} + options={({route}) => ({title: title(`Post by @${route.params.name}`)})} + /> + <Stack.Screen + name="PostLikedBy" + component={PostLikedByScreen} + options={({route}) => ({title: title(`Post by @${route.params.name}`)})} + /> + <Stack.Screen + name="PostRepostedBy" + component={PostRepostedByScreen} + options={({route}) => ({title: title(`Post by @${route.params.name}`)})} + /> + <Stack.Screen + name="Debug" + component={DebugScreen} + options={{title: title('Debug')}} + /> + <Stack.Screen + name="Log" + component={LogScreen} + options={{title: title('Log')}} + /> + <Stack.Screen + name="Support" + component={SupportScreen} + options={{title: title('Support')}} + /> + <Stack.Screen + name="PrivacyPolicy" + component={PrivacyPolicyScreen} + options={{title: title('Privacy Policy')}} + /> + <Stack.Screen + name="TermsOfService" + component={TermsOfServiceScreen} + options={{title: title('Terms of Service')}} /> - <Stack.Screen name="ProfileFollows" component={ProfileFollowsScreen} /> - <Stack.Screen name="ProfileList" component={ProfileListScreen} /> - <Stack.Screen name="PostThread" component={PostThreadScreen} /> - <Stack.Screen name="PostLikedBy" component={PostLikedByScreen} /> - <Stack.Screen name="PostRepostedBy" component={PostRepostedByScreen} /> - <Stack.Screen name="Debug" component={DebugScreen} /> - <Stack.Screen name="Log" component={LogScreen} /> - <Stack.Screen name="Support" component={SupportScreen} /> - <Stack.Screen name="PrivacyPolicy" component={PrivacyPolicyScreen} /> - <Stack.Screen name="TermsOfService" component={TermsOfServiceScreen} /> <Stack.Screen name="CommunityGuidelines" component={CommunityGuidelinesScreen} + options={{title: title('Community Guidelines')}} + /> + <Stack.Screen + name="CopyrightPolicy" + component={CopyrightPolicyScreen} + options={{title: title('Copyright Policy')}} + /> + <Stack.Screen + name="AppPasswords" + component={AppPasswords} + options={{title: title('App Passwords')}} /> - <Stack.Screen name="CopyrightPolicy" component={CopyrightPolicyScreen} /> - <Stack.Screen name="AppPasswords" component={AppPasswords} /> </> ) } @@ -221,6 +298,8 @@ const MyProfileTabNavigator = observer(() => { */ function FlatNavigator() { const pal = usePalette('default') + const unreadCountLabel = useUnreadCountLabel() + const title = (page: string) => bskyTitle(page, unreadCountLabel) return ( <Flat.Navigator screenOptions={{ @@ -230,10 +309,22 @@ function FlatNavigator() { animationDuration: 250, contentStyle: [pal.view], }}> - <Flat.Screen name="Home" component={HomeScreen} /> - <Flat.Screen name="Search" component={SearchScreen} /> - <Flat.Screen name="Notifications" component={NotificationsScreen} /> - {commonScreens(Flat as typeof HomeTab)} + <Flat.Screen + name="Home" + component={HomeScreen} + options={{title: title('Home')}} + /> + <Flat.Screen + name="Search" + component={SearchScreen} + options={{title: title('Search')}} + /> + <Flat.Screen + name="Notifications" + component={NotificationsScreen} + options={{title: title('Notifications')}} + /> + {commonScreens(Flat as typeof HomeTab, unreadCountLabel)} </Flat.Navigator> ) } diff --git a/src/lib/hooks/useSetTitle.ts b/src/lib/hooks/useSetTitle.ts new file mode 100644 index 000000000..85ba44d29 --- /dev/null +++ b/src/lib/hooks/useSetTitle.ts @@ -0,0 +1,16 @@ +import {useEffect} from 'react' +import {useNavigation} from '@react-navigation/native' + +import {NavigationProp} from 'lib/routes/types' +import {bskyTitle} from 'lib/strings/headings' +import {useUnreadCountLabel} from './useUnreadCountLabel' + +export function useSetTitle(title?: string) { + const navigation = useNavigation<NavigationProp>() + const unreadCountLabel = useUnreadCountLabel() + useEffect(() => { + if (title) { + navigation.setOptions({title: bskyTitle(title, unreadCountLabel)}) + } + }, [title, navigation, unreadCountLabel]) +} diff --git a/src/lib/hooks/useUnreadCountLabel.ts b/src/lib/hooks/useUnreadCountLabel.ts new file mode 100644 index 000000000..e2bf77885 --- /dev/null +++ b/src/lib/hooks/useUnreadCountLabel.ts @@ -0,0 +1,19 @@ +import {useEffect, useReducer} from 'react' +import {DeviceEventEmitter} from 'react-native' +import {useStores} from 'state/index' + +export function useUnreadCountLabel() { + // HACK: We don't have anything like Redux selectors, + // and we don't want to use <RootStoreContext.Consumer /> + // to react to the whole store + const [, forceUpdate] = useReducer(x => x + 1, 0) + useEffect(() => { + const subscription = DeviceEventEmitter.addListener( + 'unread-notifications', + forceUpdate, + ) + return () => subscription?.remove() + }, [forceUpdate]) + + return useStores().me.notifications.unreadCountLabel +} diff --git a/src/lib/strings/display-names.ts b/src/lib/strings/display-names.ts index 555151b55..b98153732 100644 --- a/src/lib/strings/display-names.ts +++ b/src/lib/strings/display-names.ts @@ -10,3 +10,18 @@ export function sanitizeDisplayName(str: string): string { } return '' } + +export function combinedDisplayName({ + handle, + displayName, +}: { + handle?: string + displayName?: string +}): string { + if (!handle) { + return '' + } + return displayName + ? `${sanitizeDisplayName(displayName)} (@${handle})` + : `@${handle}` +} diff --git a/src/lib/strings/headings.ts b/src/lib/strings/headings.ts new file mode 100644 index 000000000..a88a69645 --- /dev/null +++ b/src/lib/strings/headings.ts @@ -0,0 +1,4 @@ +export function bskyTitle(page: string, unreadCountLabel?: string) { + const unreadPrefix = unreadCountLabel ? `(${unreadCountLabel}) ` : '' + return `${unreadPrefix}${page} - Bluesky` +} diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index b3da0b01b..610b96507 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -24,8 +24,10 @@ import {Text} from '../util/text/Text' import {s} from 'lib/styles' import {isDesktopWeb, isMobileWeb} from 'platform/detection' import {usePalette} from 'lib/hooks/usePalette' +import {useSetTitle} from 'lib/hooks/useSetTitle' import {useNavigation} from '@react-navigation/native' import {NavigationProp} from 'lib/routes/types' +import {sanitizeDisplayName} from 'lib/strings/display-names' const REPLY_PROMPT = {_reactKey: '__reply__', _isHighlightedPost: false} const DELETED = {_reactKey: '__deleted__', _isHighlightedPost: false} @@ -59,6 +61,13 @@ export const PostThread = observer(function PostThread({ } return [] }, [view.thread]) + useSetTitle( + view.thread?.postRecord && + `${sanitizeDisplayName( + view.thread.post.author.displayName || + `@${view.thread.post.author.handle}`, + )}: "${view.thread?.postRecord?.text}"`, + ) // events // = diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index d23974859..b6d92e46b 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -25,6 +25,8 @@ import {FAB} from '../com/util/fab/FAB' import {s, colors} from 'lib/styles' import {useAnalytics} from 'lib/analytics' import {ComposeIcon2} from 'lib/icons' +import {useSetTitle} from 'lib/hooks/useSetTitle' +import {combinedDisplayName} from 'lib/strings/display-names' type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'> export const ProfileScreen = withAuthRequired( @@ -41,6 +43,7 @@ export const ProfileScreen = withAuthRequired( () => new ProfileUiModel(store, {user: route.params.name}), [route.params.name, store], ) + useSetTitle(combinedDisplayName(uiState.profile)) useFocusEffect( React.useCallback(() => { diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 3375c5e64..01f27bae1 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -14,6 +14,7 @@ import * as Toast from 'view/com/util/Toast' import {ListModel} from 'state/models/content/list' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' +import {useSetTitle} from 'lib/hooks/useSetTitle' import {NavigationProp} from 'lib/routes/types' import {isDesktopWeb} from 'platform/detection' @@ -32,6 +33,7 @@ export const ProfileListScreen = withAuthRequired( ) return model }, [store, name, rkey]) + useSetTitle(list.list?.name) useFocusEffect( React.useCallback(() => { |