An ATproto social media client -- with an independent Appview.
at main 179 lines 4.8 kB view raw
1import {useCallback, useMemo, useRef, useState} from 'react' 2import {type AppBskyUnspeccedGetPostThreadV2} from '@atproto/api' 3import debounce from 'lodash.debounce' 4 5import {OnceKey, useCallOnce} from '#/lib/hooks/useCallOnce' 6import {logger} from '#/logger' 7import { 8 usePreferencesQuery, 9 useSetThreadViewPreferencesMutation, 10} from '#/state/queries/preferences' 11import {type ThreadViewPreferences} from '#/state/queries/preferences/types' 12import {type Literal} from '#/types/utils' 13 14export type ThreadSortOption = Literal< 15 AppBskyUnspeccedGetPostThreadV2.QueryParams['sort'], 16 string 17> 18export type ThreadViewOption = 'linear' | 'tree' 19export type ThreadPreferences = { 20 isLoaded: boolean 21 isSaving: boolean 22 sort: ThreadSortOption 23 setSort: (sort: string) => void 24 view: ThreadViewOption 25 setView: (view: ThreadViewOption) => void 26 prioritizeFollowedUsers: boolean 27 setPrioritizeFollowedUsers: (prioritize: boolean) => void 28} 29 30export function useThreadPreferences({ 31 save, 32}: {save?: boolean} = {}): ThreadPreferences { 33 const {data: preferences} = usePreferencesQuery() 34 const serverPrefs = preferences?.threadViewPrefs 35 const once = useCallOnce(OnceKey.PreferencesThread) 36 37 /* 38 * Create local state representations of server state 39 */ 40 const [sort, setSort] = useState(normalizeSort(serverPrefs?.sort || 'top')) 41 const [view, setView] = useState( 42 normalizeView({ 43 treeViewEnabled: !!serverPrefs?.lab_treeViewEnabled, 44 }), 45 ) 46 const [prioritizeFollowedUsers, setPrioritizeFollowedUsers] = useState( 47 !!serverPrefs?.prioritizeFollowedUsers, 48 ) 49 50 /** 51 * If we get a server update, update local state 52 */ 53 const [prevServerPrefs, setPrevServerPrefs] = useState(serverPrefs) 54 const isLoaded = !!prevServerPrefs 55 if (serverPrefs && prevServerPrefs !== serverPrefs) { 56 setPrevServerPrefs(serverPrefs) 57 58 /* 59 * Update 60 */ 61 setSort(normalizeSort(serverPrefs.sort)) 62 setPrioritizeFollowedUsers(serverPrefs.prioritizeFollowedUsers) 63 setView( 64 normalizeView({ 65 treeViewEnabled: !!serverPrefs.lab_treeViewEnabled, 66 }), 67 ) 68 69 once(() => { 70 logger.metric('thread:preferences:load', { 71 sort: serverPrefs.sort, 72 view: serverPrefs.lab_treeViewEnabled ? 'tree' : 'linear', 73 prioritizeFollowedUsers: serverPrefs.prioritizeFollowedUsers, 74 }) 75 }) 76 } 77 78 const userUpdatedPrefs = useRef(false) 79 const [isSaving, setIsSaving] = useState(false) 80 const {mutateAsync} = useSetThreadViewPreferencesMutation() 81 const savePrefs = useMemo(() => { 82 return debounce(async (prefs: ThreadViewPreferences) => { 83 try { 84 setIsSaving(true) 85 await mutateAsync(prefs) 86 logger.metric('thread:preferences:update', { 87 sort: prefs.sort, 88 view: prefs.lab_treeViewEnabled ? 'tree' : 'linear', 89 prioritizeFollowedUsers: prefs.prioritizeFollowedUsers, 90 }) 91 } catch (e) { 92 logger.error('useThreadPreferences failed to save', { 93 safeMessage: e, 94 }) 95 } finally { 96 setIsSaving(false) 97 } 98 }, 4e3) 99 }, [mutateAsync]) 100 101 if (save && userUpdatedPrefs.current) { 102 savePrefs({ 103 sort, 104 prioritizeFollowedUsers, 105 lab_treeViewEnabled: view === 'tree', 106 }) 107 userUpdatedPrefs.current = false 108 } 109 110 const setSortWrapped = useCallback( 111 (next: string) => { 112 userUpdatedPrefs.current = true 113 setSort(normalizeSort(next)) 114 }, 115 [setSort], 116 ) 117 const setViewWrapped = useCallback( 118 (next: ThreadViewOption) => { 119 userUpdatedPrefs.current = true 120 setView(next) 121 }, 122 [setView], 123 ) 124 const setPrioritizeFollowedUsersWrapped = useCallback( 125 (next: boolean) => { 126 userUpdatedPrefs.current = true 127 setPrioritizeFollowedUsers(next) 128 }, 129 [setPrioritizeFollowedUsers], 130 ) 131 132 return useMemo( 133 () => ({ 134 isLoaded, 135 isSaving, 136 sort, 137 setSort: setSortWrapped, 138 view, 139 setView: setViewWrapped, 140 prioritizeFollowedUsers, 141 setPrioritizeFollowedUsers: setPrioritizeFollowedUsersWrapped, 142 }), 143 [ 144 isLoaded, 145 isSaving, 146 sort, 147 setSortWrapped, 148 view, 149 setViewWrapped, 150 prioritizeFollowedUsers, 151 setPrioritizeFollowedUsersWrapped, 152 ], 153 ) 154} 155 156/** 157 * Migrates user thread preferences from the old sort values to V2 158 */ 159export function normalizeSort(sort: string): ThreadSortOption { 160 switch (sort) { 161 case 'oldest': 162 return 'oldest' 163 case 'newest': 164 return 'newest' 165 default: 166 return 'top' 167 } 168} 169 170/** 171 * Transforms existing treeViewEnabled preference into a ThreadViewOption 172 */ 173export function normalizeView({ 174 treeViewEnabled, 175}: { 176 treeViewEnabled: boolean 177}): ThreadViewOption { 178 return treeViewEnabled ? 'tree' : 'linear' 179}