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