diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 4e0403be..17bb3c15 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() @@ -69,45 +71,120 @@ const Tab = createBottomTabNavigator() /** * 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 ( <> - - + + + + ({title: title(`@${route.params.name}`)})} /> - - ({ + title: title(`People following @${route.params.name}`), + })} + /> + ({ + title: title(`People followed by @${route.params.name}`), + })} + /> + + ({title: title(`Post by @${route.params.name}`)})} + /> + ({title: title(`Post by @${route.params.name}`)})} + /> + ({title: title(`Post by @${route.params.name}`)})} + /> + + + + + - - - - - - - - - - + + - - ) } @@ -221,6 +298,8 @@ const MyProfileTabNavigator = observer(() => { */ function FlatNavigator() { const pal = usePalette('default') + const unreadCountLabel = useUnreadCountLabel() + const title = (page: string) => bskyTitle(page, unreadCountLabel) return ( - - - - {commonScreens(Flat as typeof HomeTab)} + + + + {commonScreens(Flat as typeof HomeTab, unreadCountLabel)} ) } diff --git a/src/lib/hooks/useSetTitle.ts b/src/lib/hooks/useSetTitle.ts new file mode 100644 index 00000000..85ba44d2 --- /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() + 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 00000000..e2bf7788 --- /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 + // 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 555151b5..b9815373 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 00000000..a88a6964 --- /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 b3da0b01..610b9650 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 d2397485..b6d92e46 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 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 3375c5e6..01f27bae 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(() => {