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