Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at main 367 lines 12 kB view raw
1import {useCallback, useMemo, useState} from 'react' 2import {View} from 'react-native' 3import { 4 type ChatBskyConvoDefs, 5 type ChatBskyConvoListConvos, 6} from '@atproto/api' 7import {msg} from '@lingui/core/macro' 8import {useLingui} from '@lingui/react' 9import {Trans} from '@lingui/react/macro' 10import {useFocusEffect, useNavigation} from '@react-navigation/native' 11import { 12 type InfiniteData, 13 type UseInfiniteQueryResult, 14} from '@tanstack/react-query' 15 16import {useAppState} from '#/lib/appState' 17import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 18import { 19 type CommonNavigatorParams, 20 type NativeStackScreenProps, 21 type NavigationProp, 22} from '#/lib/routes/types' 23import {cleanError} from '#/lib/strings/errors' 24import {logger} from '#/logger' 25import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const' 26import {useMessagesEventBus} from '#/state/messages/events' 27import {useLeftConvos} from '#/state/queries/messages/leave-conversation' 28import {useListConvosQuery} from '#/state/queries/messages/list-conversations' 29import {useUpdateAllRead} from '#/state/queries/messages/update-all-read' 30import {FAB} from '#/view/com/util/fab/FAB' 31import {List} from '#/view/com/util/List' 32import {ChatListLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 33import {atoms as a, useBreakpoints, useTheme} from '#/alf' 34import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen' 35import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' 36import {Button, ButtonIcon, ButtonText} from '#/components/Button' 37import {useRefreshOnFocus} from '#/components/hooks/useRefreshOnFocus' 38import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow' 39import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate' 40import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' 41import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo' 42import {Message_Stroke2_Corner0_Rounded as MessageIcon} from '#/components/icons/Message' 43import * as Layout from '#/components/Layout' 44import {ListFooter} from '#/components/Lists' 45import * as Toast from '#/components/Toast' 46import {Text} from '#/components/Typography' 47import {IS_NATIVE} from '#/env' 48import {RequestListItem} from './components/RequestListItem' 49 50type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesInbox'> 51 52export function MessagesInboxScreen(props: Props) { 53 const {_} = useLingui() 54 const aaCopy = useAgeAssuranceCopy() 55 return ( 56 <AgeRestrictedScreen 57 screenTitle={_(msg`Chat requests`)} 58 infoText={aaCopy.chatsInfoText}> 59 <MessagesInboxScreenInner {...props} /> 60 </AgeRestrictedScreen> 61 ) 62} 63 64export function MessagesInboxScreenInner({}: Props) { 65 const {gtTablet} = useBreakpoints() 66 67 const listConvosQuery = useListConvosQuery({status: 'request'}) 68 const {data} = listConvosQuery 69 70 const leftConvos = useLeftConvos() 71 72 const conversations = useMemo(() => { 73 if (data?.pages) { 74 const convos = data.pages 75 .flatMap(page => page.convos) 76 // filter out convos that are actively being left 77 .filter(convo => !leftConvos.includes(convo.id)) 78 79 return convos 80 } 81 return [] 82 }, [data, leftConvos]) 83 84 const hasUnreadConvos = useMemo(() => { 85 return conversations.some( 86 conversation => 87 conversation.members.every( 88 member => member.handle !== 'missing.invalid', 89 ) && conversation.unreadCount > 0, 90 ) 91 }, [conversations]) 92 93 return ( 94 <Layout.Screen testID="messagesInboxScreen"> 95 <Layout.Header.Outer> 96 <Layout.Header.BackButton /> 97 <Layout.Header.Content align={gtTablet ? 'left' : 'platform'}> 98 <Layout.Header.TitleText> 99 <Trans>Chat requests</Trans> 100 </Layout.Header.TitleText> 101 </Layout.Header.Content> 102 {hasUnreadConvos && gtTablet ? ( 103 <MarkAsReadHeaderButton /> 104 ) : ( 105 <Layout.Header.Slot /> 106 )} 107 </Layout.Header.Outer> 108 <RequestList 109 listConvosQuery={listConvosQuery} 110 conversations={conversations} 111 hasUnreadConvos={hasUnreadConvos} 112 /> 113 </Layout.Screen> 114 ) 115} 116 117function RequestList({ 118 listConvosQuery, 119 conversations, 120 hasUnreadConvos, 121}: { 122 listConvosQuery: UseInfiniteQueryResult< 123 InfiniteData<ChatBskyConvoListConvos.OutputSchema>, 124 Error 125 > 126 conversations: ChatBskyConvoDefs.ConvoView[] 127 hasUnreadConvos: boolean 128}) { 129 const {_} = useLingui() 130 const t = useTheme() 131 const navigation = useNavigation<NavigationProp>() 132 133 // Request the poll interval to be 10s (or whatever the MESSAGE_SCREEN_POLL_INTERVAL is set to in the future) 134 // but only when the screen is active 135 const messagesBus = useMessagesEventBus() 136 const state = useAppState() 137 const isActive = state === 'active' 138 useFocusEffect( 139 useCallback(() => { 140 if (isActive) { 141 const unsub = messagesBus.requestPollInterval( 142 MESSAGE_SCREEN_POLL_INTERVAL, 143 ) 144 return () => unsub() 145 } 146 }, [messagesBus, isActive]), 147 ) 148 149 const initialNumToRender = useInitialNumToRender({minItemHeight: 130}) 150 const [isPTRing, setIsPTRing] = useState(false) 151 152 const { 153 isLoading, 154 isFetchingNextPage, 155 hasNextPage, 156 fetchNextPage, 157 isError, 158 error, 159 refetch, 160 } = listConvosQuery 161 162 useRefreshOnFocus(refetch) 163 164 const onRefresh = useCallback(async () => { 165 setIsPTRing(true) 166 try { 167 await refetch() 168 } catch (err) { 169 logger.error('Failed to refresh conversations', {message: err}) 170 } 171 setIsPTRing(false) 172 }, [refetch, setIsPTRing]) 173 174 const onEndReached = useCallback(async () => { 175 if (isFetchingNextPage || !hasNextPage || isError) return 176 try { 177 await fetchNextPage() 178 } catch (err) { 179 logger.error('Failed to load more conversations', {message: err}) 180 } 181 }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 182 183 if (conversations.length < 1) { 184 return ( 185 <Layout.Center> 186 {isLoading ? ( 187 <ChatListLoadingPlaceholder /> 188 ) : ( 189 <> 190 {isError ? ( 191 <> 192 <View style={[a.pt_3xl, a.align_center]}> 193 <CircleInfoIcon 194 width={48} 195 fill={t.atoms.text_contrast_low.color} 196 /> 197 <Text 198 style={[a.pt_md, a.pb_sm, a.text_2xl, a.font_semi_bold]}> 199 <Trans>Whoops!</Trans> 200 </Text> 201 <Text 202 style={[ 203 a.text_md, 204 a.pb_xl, 205 a.text_center, 206 a.leading_snug, 207 t.atoms.text_contrast_medium, 208 {maxWidth: 360}, 209 ]}> 210 {cleanError(error) || _(msg`Failed to load conversations`)} 211 </Text> 212 213 <Button 214 label={_(msg`Reload conversations`)} 215 size="small" 216 color="secondary_inverted" 217 variant="solid" 218 onPress={() => refetch()}> 219 <ButtonText> 220 <Trans>Retry</Trans> 221 </ButtonText> 222 <ButtonIcon icon={RetryIcon} position="right" /> 223 </Button> 224 </View> 225 </> 226 ) : ( 227 <> 228 <View style={[a.pt_3xl, a.align_center]}> 229 <MessageIcon width={48} fill={t.palette.primary_500} /> 230 <Text 231 style={[a.pt_md, a.pb_sm, a.text_2xl, a.font_semi_bold]}> 232 <Trans comment="Title message shown in chat requests inbox when it's empty"> 233 Inbox zero! 234 </Trans> 235 </Text> 236 <Text 237 style={[ 238 a.text_md, 239 a.pb_xl, 240 a.text_center, 241 a.leading_snug, 242 t.atoms.text_contrast_medium, 243 ]}> 244 <Trans> 245 You don't have any chat requests at the moment. 246 </Trans> 247 </Text> 248 <Button 249 variant="solid" 250 color="secondary" 251 size="small" 252 label={_(msg`Go back`)} 253 onPress={() => { 254 if (navigation.canGoBack()) { 255 navigation.goBack() 256 } else { 257 navigation.navigate('Messages', {animation: 'pop'}) 258 } 259 }}> 260 <ButtonIcon icon={ArrowLeftIcon} /> 261 <ButtonText> 262 <Trans>Back to Chats</Trans> 263 </ButtonText> 264 </Button> 265 </View> 266 </> 267 )} 268 </> 269 )} 270 </Layout.Center> 271 ) 272 } 273 274 return ( 275 <> 276 <List 277 data={conversations} 278 renderItem={renderItem} 279 keyExtractor={keyExtractor} 280 refreshing={isPTRing} 281 onRefresh={onRefresh} 282 onEndReached={onEndReached} 283 ListFooterComponent={ 284 <ListFooter 285 isFetchingNextPage={isFetchingNextPage} 286 error={cleanError(error)} 287 onRetry={fetchNextPage} 288 style={{borderColor: 'transparent'}} 289 hasNextPage={hasNextPage} 290 /> 291 } 292 onEndReachedThreshold={IS_NATIVE ? 1.5 : 0} 293 initialNumToRender={initialNumToRender} 294 windowSize={11} 295 desktopFixedHeight 296 sideBorders={false} 297 /> 298 {hasUnreadConvos && <MarkAllReadFAB />} 299 </> 300 ) 301} 302 303function keyExtractor(item: ChatBskyConvoDefs.ConvoView) { 304 return item.id 305} 306 307function renderItem({item}: {item: ChatBskyConvoDefs.ConvoView}) { 308 return <RequestListItem convo={item} /> 309} 310 311function MarkAllReadFAB() { 312 const {_} = useLingui() 313 const t = useTheme() 314 const {mutate: markAllRead} = useUpdateAllRead('request', { 315 onMutate: () => { 316 Toast.show(_(msg`Marked all as read`), { 317 type: 'success', 318 }) 319 }, 320 onError: () => { 321 Toast.show(_(msg`Failed to mark all requests as read`), { 322 type: 'error', 323 }) 324 }, 325 }) 326 327 return ( 328 <FAB 329 testID="markAllAsReadFAB" 330 onPress={() => markAllRead()} 331 icon={<CheckIcon size="lg" fill={t.palette.white} />} 332 accessibilityRole="button" 333 accessibilityLabel={_(msg`Mark all as read`)} 334 accessibilityHint="" 335 /> 336 ) 337} 338 339function MarkAsReadHeaderButton() { 340 const {_} = useLingui() 341 const {mutate: markAllRead} = useUpdateAllRead('request', { 342 onMutate: () => { 343 Toast.show(_(msg`Marked all as read`), { 344 type: 'success', 345 }) 346 }, 347 onError: () => { 348 Toast.show(_(msg`Failed to mark all requests as read`), { 349 type: 'error', 350 }) 351 }, 352 }) 353 354 return ( 355 <Button 356 label={_(msg`Mark all as read`)} 357 size="small" 358 color="secondary" 359 variant="solid" 360 onPress={() => markAllRead()}> 361 <ButtonIcon icon={CheckIcon} /> 362 <ButtonText> 363 <Trans>Mark all as read</Trans> 364 </ButtonText> 365 </Button> 366 ) 367}