From f93f40ce75a4b2aec1a42a697abadd8424b44c48 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 6 Jun 2025 11:41:41 -0500 Subject: [PATCH] Update settings screen --- src/screens/Settings/ThreadPreferences.tsx | 132 +++++++++++++++++- .../preferences/useThreadPreferences.ts | 99 +++++++++---- 2 files changed, 205 insertions(+), 26 deletions(-) diff --git a/src/screens/Settings/ThreadPreferences.tsx b/src/screens/Settings/ThreadPreferences.tsx index 701d3d9e56..26bf3150ea 100644 --- a/src/screens/Settings/ThreadPreferences.tsx +++ b/src/screens/Settings/ThreadPreferences.tsx @@ -2,11 +2,17 @@ import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' +import {type CommonNavigatorParams, type NativeStackScreenProps} from '#/lib/routes/types' +import {useGate} from '#/lib/statsig/statsig' import { usePreferencesQuery, useSetThreadViewPreferencesMutation, } from '#/state/queries/preferences' +import { + normalizeSort, + normalizeView, + useThreadPreferences, +} from '#/state/queries/preferences/useThreadPreferences' import {atoms as a, useTheme} from '#/alf' import * as Toggle from '#/components/forms/Toggle' import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' @@ -18,6 +24,130 @@ import * as SettingsList from './components/SettingsList' type Props = NativeStackScreenProps export function ThreadPreferencesScreen({}: Props) { + const gate = useGate() + + return gate('post_threads_v2_unspecced') ? ( + + ) : ( + + ) +} + +export function ThreadPreferencesV2() { + const t = useTheme() + const {_} = useLingui() + const { + sort, + setSort, + view, + setView, + prioritizeFollowedUsers, + setPrioritizeFollowedUsers, + } = useThreadPreferences({save: true}) + + return ( + + + + + + Thread Preferences + + + + + + + + + + Sort replies + + + + Sort replies to the same post by: + + setSort(normalizeSort(values[0]))}> + + + + + Top replies first + + + + + + Oldest replies first + + + + + + Newest replies first + + + + + + + + + + + Prioritize your Follows + + setPrioritizeFollowedUsers(value)} + style={[a.w_full, a.gap_md]}> + + + Show replies by people you follow before all other replies + + + + + + + + + + Tree view + + + setView(normalizeView({treeViewEnabled: value})) + } + style={[a.w_full, a.gap_md]}> + + Show replies as threaded + + + + + + + + ) +} + +export function ThreadPreferencesV1() { const {_} = useLingui() const t = useTheme() diff --git a/src/state/queries/preferences/useThreadPreferences.ts b/src/state/queries/preferences/useThreadPreferences.ts index 340c9975c6..96d943b2f0 100644 --- a/src/state/queries/preferences/useThreadPreferences.ts +++ b/src/state/queries/preferences/useThreadPreferences.ts @@ -1,7 +1,13 @@ -import {useMemo, useState} from 'react' +import {useCallback, useMemo, useState} from 'react' import {type AppBskyUnspeccedGetPostThreadV2} from '@atproto/api' +import debounce from 'lodash.debounce' -import {usePreferencesQuery} from '#/state/queries/preferences' +import {logger} from '#/logger' +import { + usePreferencesQuery, + useSetThreadViewPreferencesMutation, +} from '#/state/queries/preferences' +import {type ThreadViewPreferences} from '#/state/queries/preferences/types' import {type Literal} from '#/types/utils' export type ThreadSortOption = Literal< @@ -11,59 +17,101 @@ export type ThreadSortOption = Literal< export type ThreadViewOption = 'linear' | 'tree' export type ThreadPreferences = { isLoaded: boolean + isSaving: boolean sort: ThreadSortOption - setSort: (sort: ThreadSortOption) => void + setSort: (sort: string) => void view: ThreadViewOption setView: (view: ThreadViewOption) => void prioritizeFollowedUsers: boolean setPrioritizeFollowedUsers: (prioritize: boolean) => void } -export function useThreadPreferences(): ThreadPreferences { +export function useThreadPreferences({ + save, +}: {save?: boolean} = {}): ThreadPreferences { const {data: preferences} = usePreferencesQuery() - const nextThreadPreferences = preferences?.threadViewPrefs + const serverPrefs = preferences?.threadViewPrefs /* * Create local state representations of server state */ - const [sort, setSort] = useState( - migrateFromSortV1(nextThreadPreferences?.sort || 'top'), - ) + const [sort, setSort] = useState(normalizeSort(serverPrefs?.sort || 'top')) const [view, setView] = useState( - computeView({ - treeViewEnabled: !!nextThreadPreferences?.lab_treeViewEnabled, + normalizeView({ + treeViewEnabled: !!serverPrefs?.lab_treeViewEnabled, }), ) const [prioritizeFollowedUsers, setPrioritizeFollowedUsers] = useState( - !!nextThreadPreferences?.prioritizeFollowedUsers, + !!serverPrefs?.prioritizeFollowedUsers, ) /** - * Cache existing and if we get a server update, reset local state + * If we get a server update, update local state */ - const [prevServerPrefs, setPrevServerPrefs] = useState(nextThreadPreferences) - if (nextThreadPreferences && prevServerPrefs !== nextThreadPreferences) { - setPrevServerPrefs(nextThreadPreferences) + const [prevServerPrefs, setPrevServerPrefs] = useState(serverPrefs) + const isLoaded = !!prevServerPrefs + if (serverPrefs && prevServerPrefs !== serverPrefs) { + setPrevServerPrefs(serverPrefs) /* - * Reset + * Update */ - setSort(migrateFromSortV1(nextThreadPreferences.sort)) - setPrioritizeFollowedUsers(nextThreadPreferences.prioritizeFollowedUsers) + setSort(normalizeSort(serverPrefs.sort)) + setPrioritizeFollowedUsers(serverPrefs.prioritizeFollowedUsers) setView( - computeView({ - treeViewEnabled: !!nextThreadPreferences.lab_treeViewEnabled, + normalizeView({ + treeViewEnabled: !!serverPrefs.lab_treeViewEnabled, }), ) } - const isLoaded = !!prevServerPrefs + const [isSaving, setIsSaving] = useState(false) + const {mutateAsync} = useSetThreadViewPreferencesMutation() + const savePrefs = useMemo(() => { + return debounce(async (prefs: ThreadViewPreferences) => { + try { + setIsSaving(true) + await mutateAsync(prefs) + } catch (e) { + logger.error('useThreadPreferences failed to save', { + safeMessage: e, + }) + } finally { + setIsSaving(false) + } + }, 4e3) + }, [mutateAsync]) + + if (save && !isSaving) { + if ( + serverPrefs?.sort !== sort || + serverPrefs?.prioritizeFollowedUsers !== prioritizeFollowedUsers || + serverPrefs?.lab_treeViewEnabled !== (view === 'tree' ? true : false) + ) { + savePrefs({ + sort, + prioritizeFollowedUsers, + lab_treeViewEnabled: view === 'tree', + }) + } + } + + /* + * Wraped for easier migration + */ + const setSortWrapped = useCallback( + (next: string) => { + setSort(normalizeSort(next)) + }, + [setSort], + ) return useMemo( () => ({ isLoaded, + isSaving, sort, - setSort, + setSort: setSortWrapped, prioritizeFollowedUsers, setPrioritizeFollowedUsers, view, @@ -71,8 +119,9 @@ export function useThreadPreferences(): ThreadPreferences { }), [ isLoaded, + isSaving, sort, - setSort, + setSortWrapped, prioritizeFollowedUsers, setPrioritizeFollowedUsers, view, @@ -84,7 +133,7 @@ export function useThreadPreferences(): ThreadPreferences { /** * Migrates user thread preferences from the old sort values to V2 */ -function migrateFromSortV1(sort: string): ThreadSortOption { +export function normalizeSort(sort: string): ThreadSortOption { switch (sort) { case 'oldest': return 'oldest' @@ -98,7 +147,7 @@ function migrateFromSortV1(sort: string): ThreadSortOption { /** * Transforms existing treeViewEnabled preference into a ThreadViewOption */ -function computeView({ +export function normalizeView({ treeViewEnabled, }: { treeViewEnabled: boolean