diff options
author | LW <git@llllvvuu.dev> | 2023-05-16 11:13:05 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-16 13:13:05 -0500 |
commit | 50c1841a06d0502428f70bb0bb225cca70f82c20 (patch) | |
tree | 47b12090bb18e04e599a45cae7648135c772ab28 /src | |
parent | a5838694bd7debecc1d66ce8f8e4492350ea289f (diff) | |
download | voidsky-50c1841.tar.zst |
For any `Screen` that shows on desktop, `title` is "(1) ... - Bluesky" where "(1)" is the unread notification count. The titles are unlocalized and the string "Bluesky" is hardcoded, following the pattern of the rest of the app. Display names and post content are loaded into the title as effects. Tested: * all screens * screen changes / component mounts/unmounts * long posts with links and images * display name set/unset * spamming myself with notifications, clearing notifications * /profile/did:... links * lint (only my changed files), jest, e2e. New utilities: `useUnreadCountLabel`, `bskyTitle`, `combinedDisplayName`, `useSetTitle`. resolves: #626 #599
Diffstat (limited to 'src')
-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(() => { |