about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Navigation.tsx133
-rw-r--r--src/lib/hooks/useSetTitle.ts16
-rw-r--r--src/lib/hooks/useUnreadCountLabel.ts19
-rw-r--r--src/lib/strings/display-names.ts15
-rw-r--r--src/lib/strings/headings.ts4
-rw-r--r--src/view/com/post-thread/PostThread.tsx9
-rw-r--r--src/view/screens/Profile.tsx3
-rw-r--r--src/view/screens/ProfileList.tsx2
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(() => {