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