diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/components/dms/MessageMenu.tsx | 12 | ||||
-rw-r--r-- | src/screens/Messages/Conversation/MessageListError.tsx | 2 | ||||
-rw-r--r-- | src/screens/Messages/Conversation/MessagesList.tsx | 22 | ||||
-rw-r--r-- | src/screens/Messages/Conversation/index.tsx | 28 | ||||
-rw-r--r-- | src/state/messages/convo/agent.ts (renamed from src/state/messages/convo.ts) | 183 | ||||
-rw-r--r-- | src/state/messages/convo/index.tsx | 75 | ||||
-rw-r--r-- | src/state/messages/convo/types.ts | 178 | ||||
-rw-r--r-- | src/state/messages/index.tsx | 74 |
8 files changed, 296 insertions, 278 deletions
diff --git a/src/components/dms/MessageMenu.tsx b/src/components/dms/MessageMenu.tsx index d2a7d147d..3a5fa54d8 100644 --- a/src/components/dms/MessageMenu.tsx +++ b/src/components/dms/MessageMenu.tsx @@ -5,8 +5,8 @@ import {ChatBskyConvoDefs} from '@atproto-labs/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useChat} from 'state/messages' -import {ConvoStatus} from 'state/messages/convo' +import {useConvo} from 'state/messages/convo' +import {ConvoStatus} from 'state/messages/convo/types' import {useSession} from 'state/session' import * as Toast from '#/view/com/util/Toast' import {atoms as a, useTheme} from '#/alf' @@ -33,7 +33,7 @@ export let MessageMenu = ({ const {_} = useLingui() const t = useTheme() const {currentAccount} = useSession() - const chat = useChat() + const convo = useConvo() const deleteControl = usePromptControl() const retryDeleteControl = usePromptControl() @@ -48,14 +48,14 @@ export let MessageMenu = ({ }, [_, message.text]) const onDelete = React.useCallback(() => { - if (chat.status !== ConvoStatus.Ready) return + if (convo.status !== ConvoStatus.Ready) return LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - chat + convo .deleteMessage(message.id) .then(() => Toast.show(_(msg`Message deleted`))) .catch(() => retryDeleteControl.open()) - }, [_, chat, message.id, retryDeleteControl]) + }, [_, convo, message.id, retryDeleteControl]) const onReport = React.useCallback(() => { // TODO report the message diff --git a/src/screens/Messages/Conversation/MessageListError.tsx b/src/screens/Messages/Conversation/MessageListError.tsx index 7c6fd02c1..5f5df4fc9 100644 --- a/src/screens/Messages/Conversation/MessageListError.tsx +++ b/src/screens/Messages/Conversation/MessageListError.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {ConvoItem, ConvoItemError} from '#/state/messages/convo' +import {ConvoItem, ConvoItemError} from '#/state/messages/convo/types' import {atoms as a, useTheme} from '#/alf' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {InlineLinkText} from '#/components/Link' diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx index 1dc26d6c3..49e780aa2 100644 --- a/src/screens/Messages/Conversation/MessagesList.tsx +++ b/src/screens/Messages/Conversation/MessagesList.tsx @@ -11,8 +11,8 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {isIOS} from '#/platform/detection' -import {useChat} from '#/state/messages' -import {ConvoItem, ConvoStatus} from '#/state/messages/convo' +import {useConvo} from '#/state/messages/convo' +import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types' import {ScrollProvider} from 'lib/ScrollContext' import {isWeb} from 'platform/detection' import {List} from 'view/com/util/List' @@ -86,7 +86,7 @@ function onScrollToIndexFailed() { } export function MessagesList() { - const chat = useChat() + const convo = useConvo() const flatListRef = useRef<FlatList>(null) // We need to keep track of when the scroll offset is at the bottom of the list to know when to scroll as new items @@ -153,20 +153,20 @@ export function MessagesList() { // The check for `hasInitiallyScrolled` prevents an initial fetch on mount. FlatList triggers `onStartReached` // immediately on mount, since we are in fact at an offset of zero, so we have to ignore those initial calls. const onStartReached = useCallback(() => { - if (chat.status === ConvoStatus.Ready && hasInitiallyScrolled) { - chat.fetchMessageHistory() + if (convo.status === ConvoStatus.Ready && hasInitiallyScrolled) { + convo.fetchMessageHistory() } - }, [chat, hasInitiallyScrolled]) + }, [convo, hasInitiallyScrolled]) const onSendMessage = useCallback( (text: string) => { - if (chat.status === ConvoStatus.Ready) { - chat.sendMessage({ + if (convo.status === ConvoStatus.Ready) { + convo.sendMessage({ text, }) } }, - [chat], + [convo], ) const onScroll = React.useCallback( @@ -229,7 +229,7 @@ export function MessagesList() { <ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}> <List ref={flatListRef} - data={chat.items} + data={convo.items} renderItem={renderItem} keyExtractor={keyExtractor} disableVirtualization={true} @@ -248,7 +248,7 @@ export function MessagesList() { onScrollToIndexFailed={onScrollToIndexFailed} scrollEventThrottle={100} ListHeaderComponent={ - <MaybeLoader isLoading={chat.isFetchingHistory} /> + <MaybeLoader isLoading={convo.isFetchingHistory} /> } /> </ScrollProvider> diff --git a/src/screens/Messages/Conversation/index.tsx b/src/screens/Messages/Conversation/index.tsx index 7b78e7e61..db22f9e31 100644 --- a/src/screens/Messages/Conversation/index.tsx +++ b/src/screens/Messages/Conversation/index.tsx @@ -13,8 +13,8 @@ import {useGate} from '#/lib/statsig/statsig' import {useCurrentConvoId} from '#/state/messages/current-convo-id' import {BACK_HITSLOP} from 'lib/constants' import {isWeb} from 'platform/detection' -import {ChatProvider, useChat} from 'state/messages' -import {ConvoStatus} from 'state/messages/convo' +import {ConvoProvider, useConvo} from 'state/messages/convo' +import {ConvoStatus} from 'state/messages/convo/types' import {PreviewableUserAvatar} from 'view/com/util/UserAvatar' import {CenteredView} from 'view/com/util/Views' import {MessagesList} from '#/screens/Messages/Conversation/MessagesList' @@ -46,23 +46,23 @@ export function MessagesConversationScreen({route}: Props) { if (!gate('dms')) return <ClipClopGate /> return ( - <ChatProvider convoId={convoId}> + <ConvoProvider convoId={convoId}> <Inner /> - </ChatProvider> + </ConvoProvider> ) } function Inner() { - const chat = useChat() + const convo = useConvo() if ( - chat.status === ConvoStatus.Uninitialized || - chat.status === ConvoStatus.Initializing + convo.status === ConvoStatus.Uninitialized || + convo.status === ConvoStatus.Initializing ) { return <ListMaybePlaceholder isLoading /> } - if (chat.status === ConvoStatus.Error) { + if (convo.status === ConvoStatus.Error) { // TODO return ( <View> @@ -71,7 +71,7 @@ function Inner() { <Button label="Retry" onPress={() => { - chat.error.retry() + convo.error.retry() }}> <ButtonText>Retry</ButtonText> </Button> @@ -81,13 +81,13 @@ function Inner() { } /* - * Any other chat states (atm) are "ready" states + * Any other convo states (atm) are "ready" states */ return ( <KeyboardProvider> <CenteredView style={{flex: 1}} sideBorders> - <Header profile={chat.recipients[0]} /> + <Header profile={convo.recipients[0]} /> <MessagesList /> </CenteredView> </KeyboardProvider> @@ -103,7 +103,7 @@ let Header = ({ const {_} = useLingui() const {gtTablet} = useBreakpoints() const navigation = useNavigation<NavigationProp>() - const chat = useChat() + const convo = useConvo() const onPressBack = useCallback(() => { if (isWeb) { @@ -157,9 +157,9 @@ let Header = ({ {profile.displayName} </Text> </View> - {chat.status === ConvoStatus.Ready ? ( + {convo.status === ConvoStatus.Ready ? ( <ConvoMenu - convo={chat.convo} + convo={convo.convo} profile={profile} onUpdateConvo={onUpdateConvo} currentScreen="conversation" diff --git a/src/state/messages/convo.ts b/src/state/messages/convo/agent.ts index de21ef396..38a3f5e62 100644 --- a/src/state/messages/convo.ts +++ b/src/state/messages/convo/agent.ts @@ -9,178 +9,16 @@ import {nanoid} from 'nanoid/non-secure' import {logger} from '#/logger' import {isNative} from '#/platform/detection' - -export type ConvoParams = { - convoId: string - agent: BskyAgent - __tempFromUserDid: string -} - -export enum ConvoStatus { - Uninitialized = 'uninitialized', - Initializing = 'initializing', - Ready = 'ready', - Error = 'error', - Backgrounded = 'backgrounded', - Suspended = 'suspended', -} - -export enum ConvoItemError { - HistoryFailed = 'historyFailed', - PollFailed = 'pollFailed', - Network = 'network', -} - -export enum ConvoErrorCode { - InitFailed = 'initFailed', -} - -export type ConvoError = { - code: ConvoErrorCode - exception?: Error - retry: () => void -} - -export enum ConvoDispatchEvent { - Init = 'init', - Ready = 'ready', - Resume = 'resume', - Background = 'background', - Suspend = 'suspend', - Error = 'error', -} - -export type ConvoDispatch = - | { - event: ConvoDispatchEvent.Init - } - | { - event: ConvoDispatchEvent.Ready - } - | { - event: ConvoDispatchEvent.Resume - } - | { - event: ConvoDispatchEvent.Background - } - | { - event: ConvoDispatchEvent.Suspend - } - | { - event: ConvoDispatchEvent.Error - payload: ConvoError - } - -export type ConvoItem = - | { - type: 'message' | 'pending-message' - key: string - message: ChatBskyConvoDefs.MessageView - nextMessage: - | ChatBskyConvoDefs.MessageView - | ChatBskyConvoDefs.DeletedMessageView - | null - } - | { - type: 'deleted-message' - key: string - message: ChatBskyConvoDefs.DeletedMessageView - nextMessage: - | ChatBskyConvoDefs.MessageView - | ChatBskyConvoDefs.DeletedMessageView - | null - } - | { - type: 'pending-retry' - key: string - retry: () => void - } - | { - type: 'error-recoverable' - key: string - code: ConvoItemError - retry: () => void - } - -export type ConvoState = - | { - status: ConvoStatus.Uninitialized - items: [] - convo: undefined - error: undefined - sender: undefined - recipients: undefined - isFetchingHistory: false - deleteMessage: undefined - sendMessage: undefined - fetchMessageHistory: undefined - } - | { - status: ConvoStatus.Initializing - items: [] - convo: undefined - error: undefined - sender: undefined - recipients: undefined - isFetchingHistory: boolean - deleteMessage: undefined - sendMessage: undefined - fetchMessageHistory: undefined - } - | { - status: ConvoStatus.Ready - items: ConvoItem[] - convo: ChatBskyConvoDefs.ConvoView - error: undefined - sender: AppBskyActorDefs.ProfileViewBasic - recipients: AppBskyActorDefs.ProfileViewBasic[] - isFetchingHistory: boolean - deleteMessage: (messageId: string) => Promise<void> - sendMessage: ( - message: ChatBskyConvoSendMessage.InputSchema['message'], - ) => void - fetchMessageHistory: () => void - } - | { - status: ConvoStatus.Suspended - items: ConvoItem[] - convo: ChatBskyConvoDefs.ConvoView - error: undefined - sender: AppBskyActorDefs.ProfileViewBasic - recipients: AppBskyActorDefs.ProfileViewBasic[] - isFetchingHistory: boolean - deleteMessage: (messageId: string) => Promise<void> - sendMessage: ( - message: ChatBskyConvoSendMessage.InputSchema['message'], - ) => Promise<void> - fetchMessageHistory: () => Promise<void> - } - | { - status: ConvoStatus.Backgrounded - items: ConvoItem[] - convo: ChatBskyConvoDefs.ConvoView - error: undefined - sender: AppBskyActorDefs.ProfileViewBasic - recipients: AppBskyActorDefs.ProfileViewBasic[] - isFetchingHistory: boolean - deleteMessage: (messageId: string) => Promise<void> - sendMessage: ( - message: ChatBskyConvoSendMessage.InputSchema['message'], - ) => Promise<void> - fetchMessageHistory: () => Promise<void> - } - | { - status: ConvoStatus.Error - items: [] - convo: undefined - error: any - sender: undefined - recipients: undefined - isFetchingHistory: false - deleteMessage: undefined - sendMessage: undefined - fetchMessageHistory: undefined - } +import { + ConvoDispatch, + ConvoDispatchEvent, + ConvoErrorCode, + ConvoItem, + ConvoItemError, + ConvoParams, + ConvoState, + ConvoStatus, +} from '#/state/messages/convo/types' const ACTIVE_POLL_INTERVAL = 1e3 const BACKGROUND_POLL_INTERVAL = 10e3 @@ -235,7 +73,6 @@ export class Convo { private headerItems: Map<string, ConvoItem> = new Map() private isProcessingPendingMessages = false - private pendingPoll: Promise<void> | undefined private nextPoll: NodeJS.Timeout | undefined convoId: string diff --git a/src/state/messages/convo/index.tsx b/src/state/messages/convo/index.tsx new file mode 100644 index 000000000..c4fe71d30 --- /dev/null +++ b/src/state/messages/convo/index.tsx @@ -0,0 +1,75 @@ +import React, {useContext, useState, useSyncExternalStore} from 'react' +import {AppState} from 'react-native' +import {BskyAgent} from '@atproto-labs/api' +import {useFocusEffect, useIsFocused} from '@react-navigation/native' + +import {Convo} from '#/state/messages/convo/agent' +import {ConvoParams, ConvoState} from '#/state/messages/convo/types' +import {useMarkAsReadMutation} from '#/state/queries/messages/conversation' +import {useAgent} from '#/state/session' +import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage' + +const ChatContext = React.createContext<ConvoState | null>(null) + +export function useConvo() { + const ctx = useContext(ChatContext) + if (!ctx) { + throw new Error('useConvo must be used within a ConvoProvider') + } + return ctx +} + +export function ConvoProvider({ + children, + convoId, +}: Pick<ConvoParams, 'convoId'> & {children: React.ReactNode}) { + const isScreenFocused = useIsFocused() + const {serviceUrl} = useDmServiceUrlStorage() + const {getAgent} = useAgent() + const [convo] = useState( + () => + new Convo({ + convoId, + agent: new BskyAgent({ + service: serviceUrl, + }), + __tempFromUserDid: getAgent().session?.did!, + }), + ) + const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot) + const {mutate: markAsRead} = useMarkAsReadMutation() + + useFocusEffect( + React.useCallback(() => { + convo.resume() + markAsRead({convoId}) + + return () => { + convo.background() + markAsRead({convoId}) + } + }, [convo, convoId, markAsRead]), + ) + + React.useEffect(() => { + const handleAppStateChange = (nextAppState: string) => { + if (isScreenFocused) { + if (nextAppState === 'active') { + convo.resume() + } else { + convo.background() + } + + markAsRead({convoId}) + } + } + + const sub = AppState.addEventListener('change', handleAppStateChange) + + return () => { + sub.remove() + } + }, [convoId, convo, isScreenFocused, markAsRead]) + + return <ChatContext.Provider value={service}>{children}</ChatContext.Provider> +} diff --git a/src/state/messages/convo/types.ts b/src/state/messages/convo/types.ts new file mode 100644 index 000000000..cfbde6d7e --- /dev/null +++ b/src/state/messages/convo/types.ts @@ -0,0 +1,178 @@ +import {AppBskyActorDefs} from '@atproto/api' +import { + BskyAgent, + ChatBskyConvoDefs, + ChatBskyConvoSendMessage, +} from '@atproto-labs/api' + +export type ConvoParams = { + convoId: string + agent: BskyAgent + __tempFromUserDid: string +} + +export enum ConvoStatus { + Uninitialized = 'uninitialized', + Initializing = 'initializing', + Ready = 'ready', + Error = 'error', + Backgrounded = 'backgrounded', + Suspended = 'suspended', +} + +export enum ConvoItemError { + HistoryFailed = 'historyFailed', + PollFailed = 'pollFailed', + Network = 'network', +} + +export enum ConvoErrorCode { + InitFailed = 'initFailed', +} + +export type ConvoError = { + code: ConvoErrorCode + exception?: Error + retry: () => void +} + +export enum ConvoDispatchEvent { + Init = 'init', + Ready = 'ready', + Resume = 'resume', + Background = 'background', + Suspend = 'suspend', + Error = 'error', +} + +export type ConvoDispatch = + | { + event: ConvoDispatchEvent.Init + } + | { + event: ConvoDispatchEvent.Ready + } + | { + event: ConvoDispatchEvent.Resume + } + | { + event: ConvoDispatchEvent.Background + } + | { + event: ConvoDispatchEvent.Suspend + } + | { + event: ConvoDispatchEvent.Error + payload: ConvoError + } + +export type ConvoItem = + | { + type: 'message' | 'pending-message' + key: string + message: ChatBskyConvoDefs.MessageView + nextMessage: + | ChatBskyConvoDefs.MessageView + | ChatBskyConvoDefs.DeletedMessageView + | null + } + | { + type: 'deleted-message' + key: string + message: ChatBskyConvoDefs.DeletedMessageView + nextMessage: + | ChatBskyConvoDefs.MessageView + | ChatBskyConvoDefs.DeletedMessageView + | null + } + | { + type: 'pending-retry' + key: string + retry: () => void + } + | { + type: 'error-recoverable' + key: string + code: ConvoItemError + retry: () => void + } + +export type ConvoState = + | { + status: ConvoStatus.Uninitialized + items: [] + convo: undefined + error: undefined + sender: undefined + recipients: undefined + isFetchingHistory: false + deleteMessage: undefined + sendMessage: undefined + fetchMessageHistory: undefined + } + | { + status: ConvoStatus.Initializing + items: [] + convo: undefined + error: undefined + sender: undefined + recipients: undefined + isFetchingHistory: boolean + deleteMessage: undefined + sendMessage: undefined + fetchMessageHistory: undefined + } + | { + status: ConvoStatus.Ready + items: ConvoItem[] + convo: ChatBskyConvoDefs.ConvoView + error: undefined + sender: AppBskyActorDefs.ProfileViewBasic + recipients: AppBskyActorDefs.ProfileViewBasic[] + isFetchingHistory: boolean + deleteMessage: (messageId: string) => Promise<void> + sendMessage: ( + message: ChatBskyConvoSendMessage.InputSchema['message'], + ) => void + fetchMessageHistory: () => void + } + | { + status: ConvoStatus.Suspended + items: ConvoItem[] + convo: ChatBskyConvoDefs.ConvoView + error: undefined + sender: AppBskyActorDefs.ProfileViewBasic + recipients: AppBskyActorDefs.ProfileViewBasic[] + isFetchingHistory: boolean + deleteMessage: (messageId: string) => Promise<void> + sendMessage: ( + message: ChatBskyConvoSendMessage.InputSchema['message'], + ) => Promise<void> + fetchMessageHistory: () => Promise<void> + } + | { + status: ConvoStatus.Backgrounded + items: ConvoItem[] + convo: ChatBskyConvoDefs.ConvoView + error: undefined + sender: AppBskyActorDefs.ProfileViewBasic + recipients: AppBskyActorDefs.ProfileViewBasic[] + isFetchingHistory: boolean + deleteMessage: (messageId: string) => Promise<void> + sendMessage: ( + message: ChatBskyConvoSendMessage.InputSchema['message'], + ) => Promise<void> + fetchMessageHistory: () => Promise<void> + } + | { + status: ConvoStatus.Error + items: [] + convo: undefined + error: any + sender: undefined + recipients: undefined + isFetchingHistory: false + deleteMessage: undefined + sendMessage: undefined + fetchMessageHistory: undefined + } diff --git a/src/state/messages/index.tsx b/src/state/messages/index.tsx index 60538615a..205d17e8c 100644 --- a/src/state/messages/index.tsx +++ b/src/state/messages/index.tsx @@ -1,79 +1,7 @@ -import React, {useContext, useState, useSyncExternalStore} from 'react' -import {AppState} from 'react-native' -import {BskyAgent} from '@atproto-labs/api' -import {useFocusEffect, useIsFocused} from '@react-navigation/native' +import React from 'react' -import {Convo, ConvoParams, ConvoState} from '#/state/messages/convo' import {CurrentConvoIdProvider} from '#/state/messages/current-convo-id' import {MessagesEventBusProvider} from '#/state/messages/events' -import {useMarkAsReadMutation} from '#/state/queries/messages/conversation' -import {useAgent} from '#/state/session' -import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage' - -const ChatContext = React.createContext<ConvoState | null>(null) - -export function useChat() { - const ctx = useContext(ChatContext) - if (!ctx) { - throw new Error('useChat must be used within a ChatProvider') - } - return ctx -} - -export function ChatProvider({ - children, - convoId, -}: Pick<ConvoParams, 'convoId'> & {children: React.ReactNode}) { - const isScreenFocused = useIsFocused() - const {serviceUrl} = useDmServiceUrlStorage() - const {getAgent} = useAgent() - const [convo] = useState( - () => - new Convo({ - convoId, - agent: new BskyAgent({ - service: serviceUrl, - }), - __tempFromUserDid: getAgent().session?.did!, - }), - ) - const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot) - const {mutate: markAsRead} = useMarkAsReadMutation() - - useFocusEffect( - React.useCallback(() => { - convo.resume() - markAsRead({convoId}) - - return () => { - convo.background() - markAsRead({convoId}) - } - }, [convo, convoId, markAsRead]), - ) - - React.useEffect(() => { - const handleAppStateChange = (nextAppState: string) => { - if (isScreenFocused) { - if (nextAppState === 'active') { - convo.resume() - } else { - convo.background() - } - - markAsRead({convoId}) - } - } - - const sub = AppState.addEventListener('change', handleAppStateChange) - - return () => { - sub.remove() - } - }, [convoId, convo, isScreenFocused, markAsRead]) - - return <ChatContext.Provider value={service}>{children}</ChatContext.Provider> -} export function MessagesProvider({children}: {children: React.ReactNode}) { return ( |