mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Update HTML `title` on web #626 #599 (#655)

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

authored by

LW and committed by
GitHub
50c1841a a5838694

+180 -21
+112 -21
src/Navigation.tsx
··· 28 28 import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' 29 29 import {router} from './routes' 30 30 import {usePalette} from 'lib/hooks/usePalette' 31 + import {useUnreadCountLabel} from 'lib/hooks/useUnreadCountLabel' 31 32 import {useStores} from './state' 32 33 33 34 import {HomeScreen} from './view/screens/Home' ··· 55 56 import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts' 56 57 import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts' 57 58 import {getRoutingInstrumentation} from 'lib/sentry' 59 + import {bskyTitle} from 'lib/strings/headings' 58 60 59 61 const navigationRef = createNavigationContainerRef<AllNavigatorParams>() 60 62 ··· 69 71 /** 70 72 * These "common screens" are reused across stacks. 71 73 */ 72 - function commonScreens(Stack: typeof HomeTab) { 74 + function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { 75 + const title = (page: string) => bskyTitle(page, unreadCountLabel) 76 + 73 77 return ( 74 78 <> 75 - <Stack.Screen name="NotFound" component={NotFoundScreen} /> 76 - <Stack.Screen name="Moderation" component={ModerationScreen} /> 79 + <Stack.Screen 80 + name="NotFound" 81 + component={NotFoundScreen} 82 + options={{title: title('Not Found')}} 83 + /> 84 + <Stack.Screen 85 + name="Moderation" 86 + component={ModerationScreen} 87 + options={{title: title('Moderation')}} 88 + /> 77 89 <Stack.Screen 78 90 name="ModerationMuteLists" 79 91 component={ModerationMuteListsScreen} 92 + options={{title: title('Mute Lists')}} 80 93 /> 81 94 <Stack.Screen 82 95 name="ModerationMutedAccounts" 83 96 component={ModerationMutedAccounts} 97 + options={{title: title('Muted Accounts')}} 84 98 /> 85 99 <Stack.Screen 86 100 name="ModerationBlockedAccounts" 87 101 component={ModerationBlockedAccounts} 102 + options={{title: title('Blocked Accounts')}} 103 + /> 104 + <Stack.Screen 105 + name="Settings" 106 + component={SettingsScreen} 107 + options={{title: title('Settings')}} 88 108 /> 89 - <Stack.Screen name="Settings" component={SettingsScreen} /> 90 - <Stack.Screen name="Profile" component={ProfileScreen} /> 109 + <Stack.Screen 110 + name="Profile" 111 + component={ProfileScreen} 112 + options={({route}) => ({title: title(`@${route.params.name}`)})} 113 + /> 91 114 <Stack.Screen 92 115 name="ProfileFollowers" 93 116 component={ProfileFollowersScreen} 117 + options={({route}) => ({ 118 + title: title(`People following @${route.params.name}`), 119 + })} 94 120 /> 95 - <Stack.Screen name="ProfileFollows" component={ProfileFollowsScreen} /> 96 - <Stack.Screen name="ProfileList" component={ProfileListScreen} /> 97 - <Stack.Screen name="PostThread" component={PostThreadScreen} /> 98 - <Stack.Screen name="PostLikedBy" component={PostLikedByScreen} /> 99 - <Stack.Screen name="PostRepostedBy" component={PostRepostedByScreen} /> 100 - <Stack.Screen name="Debug" component={DebugScreen} /> 101 - <Stack.Screen name="Log" component={LogScreen} /> 102 - <Stack.Screen name="Support" component={SupportScreen} /> 103 - <Stack.Screen name="PrivacyPolicy" component={PrivacyPolicyScreen} /> 104 - <Stack.Screen name="TermsOfService" component={TermsOfServiceScreen} /> 121 + <Stack.Screen 122 + name="ProfileFollows" 123 + component={ProfileFollowsScreen} 124 + options={({route}) => ({ 125 + title: title(`People followed by @${route.params.name}`), 126 + })} 127 + /> 128 + <Stack.Screen 129 + name="ProfileList" 130 + component={ProfileListScreen} 131 + options={{title: title('Mute List')}} 132 + /> 133 + <Stack.Screen 134 + name="PostThread" 135 + component={PostThreadScreen} 136 + options={({route}) => ({title: title(`Post by @${route.params.name}`)})} 137 + /> 138 + <Stack.Screen 139 + name="PostLikedBy" 140 + component={PostLikedByScreen} 141 + options={({route}) => ({title: title(`Post by @${route.params.name}`)})} 142 + /> 143 + <Stack.Screen 144 + name="PostRepostedBy" 145 + component={PostRepostedByScreen} 146 + options={({route}) => ({title: title(`Post by @${route.params.name}`)})} 147 + /> 148 + <Stack.Screen 149 + name="Debug" 150 + component={DebugScreen} 151 + options={{title: title('Debug')}} 152 + /> 153 + <Stack.Screen 154 + name="Log" 155 + component={LogScreen} 156 + options={{title: title('Log')}} 157 + /> 158 + <Stack.Screen 159 + name="Support" 160 + component={SupportScreen} 161 + options={{title: title('Support')}} 162 + /> 163 + <Stack.Screen 164 + name="PrivacyPolicy" 165 + component={PrivacyPolicyScreen} 166 + options={{title: title('Privacy Policy')}} 167 + /> 168 + <Stack.Screen 169 + name="TermsOfService" 170 + component={TermsOfServiceScreen} 171 + options={{title: title('Terms of Service')}} 172 + /> 105 173 <Stack.Screen 106 174 name="CommunityGuidelines" 107 175 component={CommunityGuidelinesScreen} 176 + options={{title: title('Community Guidelines')}} 108 177 /> 109 - <Stack.Screen name="CopyrightPolicy" component={CopyrightPolicyScreen} /> 110 - <Stack.Screen name="AppPasswords" component={AppPasswords} /> 178 + <Stack.Screen 179 + name="CopyrightPolicy" 180 + component={CopyrightPolicyScreen} 181 + options={{title: title('Copyright Policy')}} 182 + /> 183 + <Stack.Screen 184 + name="AppPasswords" 185 + component={AppPasswords} 186 + options={{title: title('App Passwords')}} 187 + /> 111 188 </> 112 189 ) 113 190 } ··· 221 298 */ 222 299 function FlatNavigator() { 223 300 const pal = usePalette('default') 301 + const unreadCountLabel = useUnreadCountLabel() 302 + const title = (page: string) => bskyTitle(page, unreadCountLabel) 224 303 return ( 225 304 <Flat.Navigator 226 305 screenOptions={{ ··· 230 309 animationDuration: 250, 231 310 contentStyle: [pal.view], 232 311 }}> 233 - <Flat.Screen name="Home" component={HomeScreen} /> 234 - <Flat.Screen name="Search" component={SearchScreen} /> 235 - <Flat.Screen name="Notifications" component={NotificationsScreen} /> 236 - {commonScreens(Flat as typeof HomeTab)} 312 + <Flat.Screen 313 + name="Home" 314 + component={HomeScreen} 315 + options={{title: title('Home')}} 316 + /> 317 + <Flat.Screen 318 + name="Search" 319 + component={SearchScreen} 320 + options={{title: title('Search')}} 321 + /> 322 + <Flat.Screen 323 + name="Notifications" 324 + component={NotificationsScreen} 325 + options={{title: title('Notifications')}} 326 + /> 327 + {commonScreens(Flat as typeof HomeTab, unreadCountLabel)} 237 328 </Flat.Navigator> 238 329 ) 239 330 }
+16
src/lib/hooks/useSetTitle.ts
··· 1 + import {useEffect} from 'react' 2 + import {useNavigation} from '@react-navigation/native' 3 + 4 + import {NavigationProp} from 'lib/routes/types' 5 + import {bskyTitle} from 'lib/strings/headings' 6 + import {useUnreadCountLabel} from './useUnreadCountLabel' 7 + 8 + export function useSetTitle(title?: string) { 9 + const navigation = useNavigation<NavigationProp>() 10 + const unreadCountLabel = useUnreadCountLabel() 11 + useEffect(() => { 12 + if (title) { 13 + navigation.setOptions({title: bskyTitle(title, unreadCountLabel)}) 14 + } 15 + }, [title, navigation, unreadCountLabel]) 16 + }
+19
src/lib/hooks/useUnreadCountLabel.ts
··· 1 + import {useEffect, useReducer} from 'react' 2 + import {DeviceEventEmitter} from 'react-native' 3 + import {useStores} from 'state/index' 4 + 5 + export function useUnreadCountLabel() { 6 + // HACK: We don't have anything like Redux selectors, 7 + // and we don't want to use <RootStoreContext.Consumer /> 8 + // to react to the whole store 9 + const [, forceUpdate] = useReducer(x => x + 1, 0) 10 + useEffect(() => { 11 + const subscription = DeviceEventEmitter.addListener( 12 + 'unread-notifications', 13 + forceUpdate, 14 + ) 15 + return () => subscription?.remove() 16 + }, [forceUpdate]) 17 + 18 + return useStores().me.notifications.unreadCountLabel 19 + }
+15
src/lib/strings/display-names.ts
··· 10 10 } 11 11 return '' 12 12 } 13 + 14 + export function combinedDisplayName({ 15 + handle, 16 + displayName, 17 + }: { 18 + handle?: string 19 + displayName?: string 20 + }): string { 21 + if (!handle) { 22 + return '' 23 + } 24 + return displayName 25 + ? `${sanitizeDisplayName(displayName)} (@${handle})` 26 + : `@${handle}` 27 + }
+4
src/lib/strings/headings.ts
··· 1 + export function bskyTitle(page: string, unreadCountLabel?: string) { 2 + const unreadPrefix = unreadCountLabel ? `(${unreadCountLabel}) ` : '' 3 + return `${unreadPrefix}${page} - Bluesky` 4 + }
+9
src/view/com/post-thread/PostThread.tsx
··· 24 24 import {s} from 'lib/styles' 25 25 import {isDesktopWeb, isMobileWeb} from 'platform/detection' 26 26 import {usePalette} from 'lib/hooks/usePalette' 27 + import {useSetTitle} from 'lib/hooks/useSetTitle' 27 28 import {useNavigation} from '@react-navigation/native' 28 29 import {NavigationProp} from 'lib/routes/types' 30 + import {sanitizeDisplayName} from 'lib/strings/display-names' 29 31 30 32 const REPLY_PROMPT = {_reactKey: '__reply__', _isHighlightedPost: false} 31 33 const DELETED = {_reactKey: '__deleted__', _isHighlightedPost: false} ··· 59 61 } 60 62 return [] 61 63 }, [view.thread]) 64 + useSetTitle( 65 + view.thread?.postRecord && 66 + `${sanitizeDisplayName( 67 + view.thread.post.author.displayName || 68 + `@${view.thread.post.author.handle}`, 69 + )}: "${view.thread?.postRecord?.text}"`, 70 + ) 62 71 63 72 // events 64 73 // =
+3
src/view/screens/Profile.tsx
··· 25 25 import {s, colors} from 'lib/styles' 26 26 import {useAnalytics} from 'lib/analytics' 27 27 import {ComposeIcon2} from 'lib/icons' 28 + import {useSetTitle} from 'lib/hooks/useSetTitle' 29 + import {combinedDisplayName} from 'lib/strings/display-names' 28 30 29 31 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'> 30 32 export const ProfileScreen = withAuthRequired( ··· 41 43 () => new ProfileUiModel(store, {user: route.params.name}), 42 44 [route.params.name, store], 43 45 ) 46 + useSetTitle(combinedDisplayName(uiState.profile)) 44 47 45 48 useFocusEffect( 46 49 React.useCallback(() => {
+2
src/view/screens/ProfileList.tsx
··· 14 14 import {ListModel} from 'state/models/content/list' 15 15 import {useStores} from 'state/index' 16 16 import {usePalette} from 'lib/hooks/usePalette' 17 + import {useSetTitle} from 'lib/hooks/useSetTitle' 17 18 import {NavigationProp} from 'lib/routes/types' 18 19 import {isDesktopWeb} from 'platform/detection' 19 20 ··· 32 33 ) 33 34 return model 34 35 }, [store, name, rkey]) 36 + useSetTitle(list.list?.name) 35 37 36 38 useFocusEffect( 37 39 React.useCallback(() => {