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