forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useEffect, useMemo} from 'react'
2import {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, select, 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 control.close(() => {
58 onSelectDraft(summary)
59 })
60 },
61 [control, onSelectDraft],
62 )
63
64 const handleDeleteDraft = useCallback(
65 (draftSummary: DraftSummary) => {
66 // Fire draft:delete metric
67 const draftAgeMs = Date.now() - new Date(draftSummary.createdAt).getTime()
68 ax.metric('draft:delete', {
69 logContext: 'DraftsList',
70 draftAgeMs,
71 })
72 deleteDraft({draftId: draftSummary.id, draft: draftSummary.draft})
73 },
74 [deleteDraft, ax],
75 )
76
77 const backButton = useCallback(
78 () => (
79 <Button
80 label={_(msg`Back`)}
81 onPress={() => control.close()}
82 size="small"
83 color="primary"
84 variant="ghost">
85 <ButtonText style={[a.text_md]}>
86 <Trans>Back</Trans>
87 </ButtonText>
88 </Button>
89 ),
90 [control, _],
91 )
92
93 const renderItem = useCallback(
94 ({item}: {item: DraftSummary}) => {
95 return (
96 <View style={[gtPhone ? [a.px_md, a.pt_md] : [a.px_sm, a.pt_sm]]}>
97 <DraftItem
98 draft={item}
99 onSelect={handleSelectDraft}
100 onDelete={handleDeleteDraft}
101 />
102 </View>
103 )
104 },
105 [handleSelectDraft, handleDeleteDraft, gtPhone],
106 )
107
108 const header = useMemo(
109 () => (
110 <Dialog.Header renderLeft={backButton}>
111 <Dialog.HeaderText>
112 <Trans>Drafts</Trans>
113 </Dialog.HeaderText>
114 </Dialog.Header>
115 ),
116 [backButton],
117 )
118
119 const onEndReached = useCallback(() => {
120 if (hasNextPage && !isFetchingNextPage) {
121 void fetchNextPage()
122 }
123 }, [hasNextPage, isFetchingNextPage, fetchNextPage])
124
125 const emptyComponent = useMemo(() => {
126 if (isLoading) {
127 return (
128 <View style={[a.py_xl, a.align_center]}>
129 <Loader size="lg" />
130 </View>
131 )
132 }
133 return (
134 <EmptyState
135 icon={PageXIcon}
136 message={_(msg`No drafts yet`)}
137 style={[a.justify_center, {minHeight: 500}]}
138 />
139 )
140 }, [isLoading, _])
141
142 const footerComponent = useMemo(
143 () => (
144 <>
145 {drafts.length > 5 && (
146 <View style={[a.align_center, a.py_2xl]}>
147 <Text style={[a.text_center, t.atoms.text_contrast_medium]}>
148 <Trans>So many thoughts, you should post one</Trans>
149 </Text>
150 </View>
151 )}
152 <ListFooter
153 isFetchingNextPage={isFetchingNextPage}
154 hasNextPage={hasNextPage}
155 style={[a.border_transparent]}
156 />
157 </>
158 ),
159 [isFetchingNextPage, hasNextPage, drafts.length, t],
160 )
161
162 return (
163 <Dialog.Outer control={control}>
164 {/* We really really need to figure out a nice, consistent API for doing a header cross-platform -sfn */}
165 {IS_NATIVE && header}
166 <Dialog.InnerFlatList
167 data={drafts}
168 renderItem={renderItem}
169 keyExtractor={(item: DraftSummary) => item.id}
170 ListHeaderComponent={web(header)}
171 stickyHeaderIndices={web([0])}
172 ListEmptyComponent={emptyComponent}
173 ListFooterComponent={footerComponent}
174 onEndReached={onEndReached}
175 onEndReachedThreshold={0.5}
176 style={[
177 a.px_0,
178 web({minHeight: 500}),
179 {
180 backgroundColor: select(t.name, {
181 light: t.palette.contrast_50,
182 dark: t.palette.contrast_0,
183 dim: '#000000',
184 }),
185 },
186 ]}
187 webInnerContentContainerStyle={[a.py_0]}
188 contentContainerStyle={[a.pb_xl]}
189 />
190 </Dialog.Outer>
191 )
192}