Bluesky app fork with some witchin' additions 馃挮
at viewport 165 lines 4.1 kB view raw
1import {useCallback, useMemo, useRef, useState} from 'react' 2import {type AppBskyUnspeccedGetPostThreadV2} from '@atproto/api' 3import {useFocusEffect} from '@react-navigation/native' 4import debounce from 'lodash.debounce' 5 6import {useCallOnce} from '#/lib/once' 7import { 8 usePreferencesQuery, 9 useSetThreadViewPreferencesMutation, 10} from '#/state/queries/preferences' 11import {type ThreadViewPreferences} from '#/state/queries/preferences/types' 12import {useAnalytics} from '#/analytics' 13import {type Literal} from '#/types/utils' 14 15export type ThreadSortOption = Literal< 16 AppBskyUnspeccedGetPostThreadV2.QueryParams['sort'], 17 string 18> 19export type ThreadViewOption = 'linear' | 'tree' 20export type ThreadPreferences = { 21 isLoaded: boolean 22 isSaving: boolean 23 sort: ThreadSortOption 24 setSort: (sort: string) => void 25 view: ThreadViewOption 26 setView: (view: ThreadViewOption) => void 27} 28 29export function useThreadPreferences({ 30 save, 31}: {save?: boolean} = {}): ThreadPreferences { 32 const ax = useAnalytics() 33 const {data: preferences} = usePreferencesQuery() 34 const serverPrefs = preferences?.threadViewPrefs 35 const once = useCallOnce() 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 47 /** 48 * If we get a server update, update local state 49 */ 50 const [prevServerPrefs, setPrevServerPrefs] = useState(serverPrefs) 51 const isLoaded = !!prevServerPrefs 52 if (serverPrefs && prevServerPrefs !== serverPrefs) { 53 setPrevServerPrefs(serverPrefs) 54 55 /* 56 * Update 57 */ 58 setSort(normalizeSort(serverPrefs.sort)) 59 setView( 60 normalizeView({ 61 treeViewEnabled: !!serverPrefs.lab_treeViewEnabled, 62 }), 63 ) 64 65 once(() => { 66 ax.metric('thread:preferences:load', { 67 sort: serverPrefs.sort, 68 view: serverPrefs.lab_treeViewEnabled ? 'tree' : 'linear', 69 }) 70 }) 71 } 72 73 const userUpdatedPrefs = useRef(false) 74 const {mutate, isPending: isSaving} = useSetThreadViewPreferencesMutation({ 75 onSuccess: (_data, prefs) => { 76 ax.metric('thread:preferences:update', { 77 sort: prefs.sort, 78 view: prefs.lab_treeViewEnabled ? 'tree' : 'linear', 79 }) 80 }, 81 onError: err => { 82 ax.logger.error('useThreadPreferences failed to save', { 83 safeMessage: err, 84 }) 85 }, 86 }) 87 const savePrefs = useMemo(() => { 88 return debounce( 89 (prefs: ThreadViewPreferences) => { 90 mutate(prefs) 91 }, 92 2e3, 93 {leading: true, trailing: true}, 94 ) 95 }, [mutate]) 96 97 // flush on leave screen 98 useFocusEffect( 99 useCallback(() => { 100 return () => { 101 void savePrefs.flush() 102 } 103 }, [savePrefs]), 104 ) 105 106 if (save && userUpdatedPrefs.current) { 107 savePrefs({ 108 sort, 109 lab_treeViewEnabled: view === 'tree', 110 }) 111 userUpdatedPrefs.current = false 112 } 113 114 const setSortWrapped = useCallback( 115 (next: string) => { 116 userUpdatedPrefs.current = true 117 setSort(normalizeSort(next)) 118 }, 119 [setSort], 120 ) 121 const setViewWrapped = useCallback( 122 (next: ThreadViewOption) => { 123 userUpdatedPrefs.current = true 124 setView(next) 125 }, 126 [setView], 127 ) 128 129 return useMemo( 130 () => ({ 131 isLoaded, 132 isSaving, 133 sort, 134 setSort: setSortWrapped, 135 view, 136 setView: setViewWrapped, 137 }), 138 [isLoaded, isSaving, sort, setSortWrapped, view, setViewWrapped], 139 ) 140} 141 142/** 143 * Migrates user thread preferences from the old sort values to V2 144 */ 145export function normalizeSort(sort: string): ThreadSortOption { 146 switch (sort) { 147 case 'oldest': 148 return 'oldest' 149 case 'newest': 150 return 'newest' 151 default: 152 return 'top' 153 } 154} 155 156/** 157 * Transforms existing treeViewEnabled preference into a ThreadViewOption 158 */ 159export function normalizeView({ 160 treeViewEnabled, 161}: { 162 treeViewEnabled: boolean 163}): ThreadViewOption { 164 return treeViewEnabled ? 'tree' : 'linear' 165}