Update settings screen

This commit is contained in:
Eric Bailey
2025-06-06 11:41:41 -05:00
parent 83b15a47f3
commit f93f40ce75
2 changed files with 205 additions and 26 deletions
+131 -1
View File
@@ -2,11 +2,17 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' 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 { import {
usePreferencesQuery, usePreferencesQuery,
useSetThreadViewPreferencesMutation, useSetThreadViewPreferencesMutation,
} from '#/state/queries/preferences' } from '#/state/queries/preferences'
import {
normalizeSort,
normalizeView,
useThreadPreferences,
} from '#/state/queries/preferences/useThreadPreferences'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
@@ -18,6 +24,130 @@ import * as SettingsList from './components/SettingsList'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
export function ThreadPreferencesScreen({}: Props) { export function ThreadPreferencesScreen({}: Props) {
const gate = useGate()
return gate('post_threads_v2_unspecced') ? (
<ThreadPreferencesV2 />
) : (
<ThreadPreferencesV1 />
)
}
export function ThreadPreferencesV2() {
const t = useTheme()
const {_} = useLingui()
const {
sort,
setSort,
view,
setView,
prioritizeFollowedUsers,
setPrioritizeFollowedUsers,
} = useThreadPreferences({save: true})
return (
<Layout.Screen testID="threadPreferencesScreen">
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Thread Preferences</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<SettingsList.Container>
<SettingsList.Group>
<SettingsList.ItemIcon icon={BubblesIcon} />
<SettingsList.ItemText>
<Trans>Sort replies</Trans>
</SettingsList.ItemText>
<View style={[a.w_full, a.gap_md]}>
<Text style={[a.flex_1, t.atoms.text_contrast_medium]}>
<Trans>Sort replies to the same post by:</Trans>
</Text>
<Toggle.Group
label={_(msg`Sort replies by`)}
type="radio"
values={sort ? [sort] : []}
onChange={values => setSort(normalizeSort(values[0]))}>
<View style={[a.gap_sm, a.flex_1]}>
<Toggle.Item name="top" label={_(msg`Top replies first`)}>
<Toggle.Radio />
<Toggle.LabelText>
<Trans>Top replies first</Trans>
</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item
name="oldest"
label={_(msg`Oldest replies first`)}>
<Toggle.Radio />
<Toggle.LabelText>
<Trans>Oldest replies first</Trans>
</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item
name="newest"
label={_(msg`Newest replies first`)}>
<Toggle.Radio />
<Toggle.LabelText>
<Trans>Newest replies first</Trans>
</Toggle.LabelText>
</Toggle.Item>
</View>
</Toggle.Group>
</View>
</SettingsList.Group>
<SettingsList.Group contentContainerStyle={{minHeight: 0}}>
<SettingsList.ItemIcon icon={PersonGroupIcon} />
<SettingsList.ItemText>
<Trans>Prioritize your Follows</Trans>
</SettingsList.ItemText>
<Toggle.Item
type="checkbox"
name="prioritize-follows"
label={_(msg`Prioritize your Follows`)}
value={prioritizeFollowedUsers}
onChange={value => setPrioritizeFollowedUsers(value)}
style={[a.w_full, a.gap_md]}>
<Toggle.LabelText style={[a.flex_1]}>
<Trans>
Show replies by people you follow before all other replies
</Trans>
</Toggle.LabelText>
<Toggle.Platform />
</Toggle.Item>
</SettingsList.Group>
<SettingsList.Group>
<SettingsList.ItemIcon icon={BeakerIcon} />
<SettingsList.ItemText>
<Trans>Tree view</Trans>
</SettingsList.ItemText>
<Toggle.Item
type="checkbox"
name="threaded-mode"
label={_(msg`Threaded mode`)}
value={view === 'tree'}
onChange={value =>
setView(normalizeView({treeViewEnabled: value}))
}
style={[a.w_full, a.gap_md]}>
<Toggle.LabelText style={[a.flex_1]}>
<Trans>Show replies as threaded</Trans>
</Toggle.LabelText>
<Toggle.Platform />
</Toggle.Item>
</SettingsList.Group>
</SettingsList.Container>
</Layout.Content>
</Layout.Screen>
)
}
export function ThreadPreferencesV1() {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
@@ -1,7 +1,13 @@
import {useMemo, useState} from 'react' import {useCallback, useMemo, useState} from 'react'
import {type AppBskyUnspeccedGetPostThreadV2} from '@atproto/api' 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' import {type Literal} from '#/types/utils'
export type ThreadSortOption = Literal< export type ThreadSortOption = Literal<
@@ -11,59 +17,101 @@ export type ThreadSortOption = Literal<
export type ThreadViewOption = 'linear' | 'tree' export type ThreadViewOption = 'linear' | 'tree'
export type ThreadPreferences = { export type ThreadPreferences = {
isLoaded: boolean isLoaded: boolean
isSaving: boolean
sort: ThreadSortOption sort: ThreadSortOption
setSort: (sort: ThreadSortOption) => void setSort: (sort: string) => void
view: ThreadViewOption view: ThreadViewOption
setView: (view: ThreadViewOption) => void setView: (view: ThreadViewOption) => void
prioritizeFollowedUsers: boolean prioritizeFollowedUsers: boolean
setPrioritizeFollowedUsers: (prioritize: boolean) => void setPrioritizeFollowedUsers: (prioritize: boolean) => void
} }
export function useThreadPreferences(): ThreadPreferences { export function useThreadPreferences({
save,
}: {save?: boolean} = {}): ThreadPreferences {
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
const nextThreadPreferences = preferences?.threadViewPrefs const serverPrefs = preferences?.threadViewPrefs
/* /*
* Create local state representations of server state * Create local state representations of server state
*/ */
const [sort, setSort] = useState( const [sort, setSort] = useState(normalizeSort(serverPrefs?.sort || 'top'))
migrateFromSortV1(nextThreadPreferences?.sort || 'top'),
)
const [view, setView] = useState( const [view, setView] = useState(
computeView({ normalizeView({
treeViewEnabled: !!nextThreadPreferences?.lab_treeViewEnabled, treeViewEnabled: !!serverPrefs?.lab_treeViewEnabled,
}), }),
) )
const [prioritizeFollowedUsers, setPrioritizeFollowedUsers] = useState( 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) const [prevServerPrefs, setPrevServerPrefs] = useState(serverPrefs)
if (nextThreadPreferences && prevServerPrefs !== nextThreadPreferences) { const isLoaded = !!prevServerPrefs
setPrevServerPrefs(nextThreadPreferences) if (serverPrefs && prevServerPrefs !== serverPrefs) {
setPrevServerPrefs(serverPrefs)
/* /*
* Reset * Update
*/ */
setSort(migrateFromSortV1(nextThreadPreferences.sort)) setSort(normalizeSort(serverPrefs.sort))
setPrioritizeFollowedUsers(nextThreadPreferences.prioritizeFollowedUsers) setPrioritizeFollowedUsers(serverPrefs.prioritizeFollowedUsers)
setView( setView(
computeView({ normalizeView({
treeViewEnabled: !!nextThreadPreferences.lab_treeViewEnabled, 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( return useMemo(
() => ({ () => ({
isLoaded, isLoaded,
isSaving,
sort, sort,
setSort, setSort: setSortWrapped,
prioritizeFollowedUsers, prioritizeFollowedUsers,
setPrioritizeFollowedUsers, setPrioritizeFollowedUsers,
view, view,
@@ -71,8 +119,9 @@ export function useThreadPreferences(): ThreadPreferences {
}), }),
[ [
isLoaded, isLoaded,
isSaving,
sort, sort,
setSort, setSortWrapped,
prioritizeFollowedUsers, prioritizeFollowedUsers,
setPrioritizeFollowedUsers, setPrioritizeFollowedUsers,
view, view,
@@ -84,7 +133,7 @@ export function useThreadPreferences(): ThreadPreferences {
/** /**
* Migrates user thread preferences from the old sort values to V2 * Migrates user thread preferences from the old sort values to V2
*/ */
function migrateFromSortV1(sort: string): ThreadSortOption { export function normalizeSort(sort: string): ThreadSortOption {
switch (sort) { switch (sort) {
case 'oldest': case 'oldest':
return 'oldest' return 'oldest'
@@ -98,7 +147,7 @@ function migrateFromSortV1(sort: string): ThreadSortOption {
/** /**
* Transforms existing treeViewEnabled preference into a ThreadViewOption * Transforms existing treeViewEnabled preference into a ThreadViewOption
*/ */
function computeView({ export function normalizeView({
treeViewEnabled, treeViewEnabled,
}: { }: {
treeViewEnabled: boolean treeViewEnabled: boolean