Bluesky app fork with some witchin' additions 馃挮
at main 188 lines 5.6 kB view raw
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}