forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useEffect, useMemo} from 'react'
2import {Keyboard, View} from 'react-native'
3import {msg} from '@lingui/core/macro'
4import {useLingui} from '@lingui/react'
5import {Trans} from '@lingui/react/macro'
6
7import {useCallOnce} from '#/lib/once'
8import {EmptyState} from '#/view/com/util/EmptyState'
9import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
10import {Button, ButtonText} from '#/components/Button'
11import * as Dialog from '#/components/Dialog'
12import {PageX_Stroke2_Corner0_Rounded_Large as PageXIcon} from '#/components/icons/PageX'
13import {ListFooter} from '#/components/Lists'
14import {Loader} from '#/components/Loader'
15import {Text} from '#/components/Typography'
16import {useAnalytics} from '#/analytics'
17import {IS_NATIVE} from '#/env'
18import {DraftItem} from './DraftItem'
19import {useDeleteDraftMutation, useDraftsQuery} from './state/queries'
20import {type DraftSummary} from './state/schema'
21
22export function DraftsListDialog({
23 control,
24 onSelectDraft,
25}: {
26 control: Dialog.DialogControlProps
27 onSelectDraft: (draft: DraftSummary) => void
28}) {
29 const {_} = useLingui()
30 const t = useTheme()
31 const {gtPhone} = useBreakpoints()
32 const ax = useAnalytics()
33 const {data, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage} =
34 useDraftsQuery()
35 const {mutate: deleteDraft} = useDeleteDraftMutation()
36
37 const drafts = useMemo(
38 () => data?.pages.flatMap(page => page.drafts) ?? [],
39 [data],
40 )
41
42 // Fire draft:listOpen metric when dialog opens and data is loaded
43 const draftCount = drafts.length
44 const isDataReady = !isLoading && data !== undefined
45 const onDraftListOpen = useCallOnce()
46 useEffect(() => {
47 if (isDataReady) {
48 onDraftListOpen(() => {
49 ax.metric('draft:listOpen', {
50 draftCount,
51 })
52 })
53 }
54 }, [onDraftListOpen, isDataReady, draftCount, ax])
55
56 const handleSelectDraft = useCallback(
57 (summary: DraftSummary) => {
58 // Dismiss keyboard immediately to prevent flicker. Without this,
59 // the text input regains focus (showing the keyboard) after the
60 // drafts sheet closes, then loses it again when the post component
61 // remounts with the draft content, causing a show-hide-show cycle -sfn
62 Keyboard.dismiss()
63
64 control.close(() => {
65 onSelectDraft(summary)
66 })
67 },
68 [control, onSelectDraft],
69 )
70
71 const handleDeleteDraft = useCallback(
72 (draftSummary: DraftSummary) => {
73 // Fire draft:delete metric
74 const draftAgeMs = Date.now() - new Date(draftSummary.createdAt).getTime()
75 ax.metric('draft:delete', {
76 logContext: 'DraftsList',
77 draftAgeMs,
78 })
79 deleteDraft({draftId: draftSummary.id, draft: draftSummary.draft})
80 },
81 [deleteDraft, ax],
82 )
83
84 const backButton = useCallback(
85 () => (
86 <Button
87 label={_(msg`Back`)}
88 onPress={() => control.close()}
89 size="small"
90 color="primary"
91 variant="ghost">
92 <ButtonText style={[a.text_md]}>
93 <Trans>Back</Trans>
94 </ButtonText>
95 </Button>
96 ),
97 [control, _],
98 )
99
100 const renderItem = useCallback(
101 ({item}: {item: DraftSummary}) => {
102 return (
103 <View style={[gtPhone ? [a.px_md, a.pt_md] : [a.px_sm, a.pt_sm]]}>
104 <DraftItem
105 draft={item}
106 onSelect={handleSelectDraft}
107 onDelete={handleDeleteDraft}
108 />
109 </View>
110 )
111 },
112 [handleSelectDraft, handleDeleteDraft, gtPhone],
113 )
114
115 const header = useMemo(
116 () => (
117 <Dialog.Header renderLeft={backButton}>
118 <Dialog.HeaderText>
119 <Trans>Drafts</Trans>
120 </Dialog.HeaderText>
121 </Dialog.Header>
122 ),
123 [backButton],
124 )
125
126 const onEndReached = useCallback(() => {
127 if (hasNextPage && !isFetchingNextPage) {
128 void fetchNextPage()
129 }
130 }, [hasNextPage, isFetchingNextPage, fetchNextPage])
131
132 const emptyComponent = useMemo(() => {
133 if (isLoading) {
134 return (
135 <View style={[a.py_xl, a.align_center]}>
136 <Loader size="lg" />
137 </View>
138 )
139 }
140 return (
141 <EmptyState
142 icon={PageXIcon}
143 message={_(msg`No drafts yet`)}
144 style={[a.justify_center, {minHeight: 500}]}
145 />
146 )
147 }, [isLoading, _])
148
149 const footerComponent = useMemo(
150 () => (
151 <>
152 {drafts.length > 5 && (
153 <View style={[a.align_center, a.py_2xl]}>
154 <Text style={[a.text_center, t.atoms.text_contrast_medium]}>
155 <Trans>So many thoughts, you should post one</Trans>
156 </Text>
157 </View>
158 )}
159 <ListFooter
160 isFetchingNextPage={isFetchingNextPage}
161 hasNextPage={hasNextPage}
162 style={[a.border_transparent]}
163 />
164 </>
165 ),
166 [isFetchingNextPage, hasNextPage, drafts.length, t],
167 )
168
169 return (
170 <Dialog.Outer control={control}>
171 {/* We really really need to figure out a nice, consistent API for doing a header cross-platform -sfn */}
172 {IS_NATIVE && header}
173 <Dialog.InnerFlatList
174 data={drafts}
175 renderItem={renderItem}
176 keyExtractor={(item: DraftSummary) => item.id}
177 ListHeaderComponent={web(header)}
178 stickyHeaderIndices={web([0])}
179 ListEmptyComponent={emptyComponent}
180 ListFooterComponent={footerComponent}
181 onEndReached={onEndReached}
182 onEndReachedThreshold={0.5}
183 style={[a.px_0, web({minHeight: 500})]}
184 webInnerContentContainerStyle={[a.py_0]}
185 contentContainerStyle={[a.pb_xl]}
186 />
187 </Dialog.Outer>
188 )
189}