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(() => {
|