Bluesky app fork with some witchin' additions 馃挮
at feat/tealfm 153 lines 3.9 kB view raw
1import {useCallback, useMemo, useRef, useState} from 'react' 2import {type AppBskyUnspeccedGetPostThreadV2} from '@atproto/api' 3import debounce from 'lodash.debounce' 4 5import {useCallOnce} from '#/lib/once' 6import { 7 usePreferencesQuery, 8 useSetThreadViewPreferencesMutation, 9} from '#/state/queries/preferences' 10import {type ThreadViewPreferences} from '#/state/queries/preferences/types' 11import {useAnalytics} from '#/analytics' 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} 27 28export function useThreadPreferences({ 29 save, 30}: {save?: boolean} = {}): ThreadPreferences { 31 const ax = useAnalytics() 32 const {data: preferences} = usePreferencesQuery() 33 const serverPrefs = preferences?.threadViewPrefs 34 const once = useCallOnce() 35 36 /* 37 * Create local state representations of server state 38 */ 39 const [sort, setSort] = useState(normalizeSort(serverPrefs?.sort || 'top')) 40 const [view, setView] = useState( 41 normalizeView({ 42 treeViewEnabled: !!serverPrefs?.lab_treeViewEnabled, 43 }), 44 ) 45 46 /** 47 * If we get a server update, update local state 48 */ 49 const [prevServerPrefs, setPrevServerPrefs] = useState(serverPrefs) 50 const isLoaded = !!prevServerPrefs 51 if (serverPrefs && prevServerPrefs !== serverPrefs) { 52 setPrevServerPrefs(serverPrefs) 53 54 /* 55 * Update 56 */ 57 setSort(normalizeSort(serverPrefs.sort)) 58 setView( 59 normalizeView({ 60 treeViewEnabled: !!serverPrefs.lab_treeViewEnabled, 61 }), 62 ) 63 64 once(() => { 65 ax.metric('thread:preferences:load', { 66 sort: serverPrefs.sort, 67 view: serverPrefs.lab_treeViewEnabled ? 'tree' : 'linear', 68 }) 69 }) 70 } 71 72 const userUpdatedPrefs = useRef(false) 73 const [isSaving, setIsSaving] = useState(false) 74 const {mutateAsync} = useSetThreadViewPreferencesMutation() 75 const savePrefs = useMemo(() => { 76 return debounce(async (prefs: ThreadViewPreferences) => { 77 try { 78 setIsSaving(true) 79 await mutateAsync(prefs) 80 ax.metric('thread:preferences:update', { 81 sort: prefs.sort, 82 view: prefs.lab_treeViewEnabled ? 'tree' : 'linear', 83 }) 84 } catch (e) { 85 ax.logger.error('useThreadPreferences failed to save', { 86 safeMessage: e, 87 }) 88 } finally { 89 setIsSaving(false) 90 } 91 }, 4e3) 92 }, [mutateAsync]) 93 94 if (save && userUpdatedPrefs.current) { 95 savePrefs({ 96 sort, 97 lab_treeViewEnabled: view === 'tree', 98 }) 99 userUpdatedPrefs.current = false 100 } 101 102 const setSortWrapped = useCallback( 103 (next: string) => { 104 userUpdatedPrefs.current = true 105 setSort(normalizeSort(next)) 106 }, 107 [setSort], 108 ) 109 const setViewWrapped = useCallback( 110 (next: ThreadViewOption) => { 111 userUpdatedPrefs.current = true 112 setView(next) 113 }, 114 [setView], 115 ) 116 117 return useMemo( 118 () => ({ 119 isLoaded, 120 isSaving, 121 sort, 122 setSort: setSortWrapped, 123 view, 124 setView: setViewWrapped, 125 }), 126 [isLoaded, isSaving, sort, setSortWrapped, view, setViewWrapped], 127 ) 128} 129 130/** 131 * Migrates user thread preferences from the old sort values to V2 132 */ 133export function normalizeSort(sort: string): ThreadSortOption { 134 switch (sort) { 135 case 'oldest': 136 return 'oldest' 137 case 'newest': 138 return 'newest' 139 default: 140 return 'top' 141 } 142} 143 144/** 145 * Transforms existing treeViewEnabled preference into a ThreadViewOption 146 */ 147export function normalizeView({ 148 treeViewEnabled, 149}: { 150 treeViewEnabled: boolean 151}): ThreadViewOption { 152 return treeViewEnabled ? 'tree' : 'linear' 153}