Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
120
fork

Configure Feed

Select the types of activity you want to include in your feed.

at a876aae44ea07494ebea9727350aa060b81f317b 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} nativeOptions={{fullHeight: true}}> 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}