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 {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}