[🐴] Reorg convo files (#3909)

* Remove unused prop

* Reorganize
zio/stable
Eric Bailey 2024-05-07 21:46:59 -05:00 committed by GitHub
parent 814ec2bd7f
commit 56f713077f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 296 additions and 278 deletions

View File

@ -5,8 +5,8 @@ import {ChatBskyConvoDefs} from '@atproto-labs/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useChat} from 'state/messages' import {useConvo} from 'state/messages/convo'
import {ConvoStatus} from 'state/messages/convo' import {ConvoStatus} from 'state/messages/convo/types'
import {useSession} from 'state/session' import {useSession} from 'state/session'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
@ -33,7 +33,7 @@ export let MessageMenu = ({
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const chat = useChat() const convo = useConvo()
const deleteControl = usePromptControl() const deleteControl = usePromptControl()
const retryDeleteControl = usePromptControl() const retryDeleteControl = usePromptControl()
@ -48,14 +48,14 @@ export let MessageMenu = ({
}, [_, message.text]) }, [_, message.text])
const onDelete = React.useCallback(() => { const onDelete = React.useCallback(() => {
if (chat.status !== ConvoStatus.Ready) return if (convo.status !== ConvoStatus.Ready) return
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
chat convo
.deleteMessage(message.id) .deleteMessage(message.id)
.then(() => Toast.show(_(msg`Message deleted`))) .then(() => Toast.show(_(msg`Message deleted`)))
.catch(() => retryDeleteControl.open()) .catch(() => retryDeleteControl.open())
}, [_, chat, message.id, retryDeleteControl]) }, [_, convo, message.id, retryDeleteControl])
const onReport = React.useCallback(() => { const onReport = React.useCallback(() => {
// TODO report the message // TODO report the message

View File

@ -3,7 +3,7 @@ import {View} from 'react-native'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' 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 {atoms as a, useTheme} from '#/alf'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'

View File

@ -11,8 +11,8 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {isIOS} from '#/platform/detection' import {isIOS} from '#/platform/detection'
import {useChat} from '#/state/messages' import {useConvo} from '#/state/messages/convo'
import {ConvoItem, ConvoStatus} from '#/state/messages/convo' import {ConvoItem, ConvoStatus} from '#/state/messages/convo/types'
import {ScrollProvider} from 'lib/ScrollContext' import {ScrollProvider} from 'lib/ScrollContext'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {List} from 'view/com/util/List' import {List} from 'view/com/util/List'
@ -86,7 +86,7 @@ function onScrollToIndexFailed() {
} }
export function MessagesList() { export function MessagesList() {
const chat = useChat() const convo = useConvo()
const flatListRef = useRef<FlatList>(null) 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 // 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` // 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. // immediately on mount, since we are in fact at an offset of zero, so we have to ignore those initial calls.
const onStartReached = useCallback(() => { const onStartReached = useCallback(() => {
if (chat.status === ConvoStatus.Ready && hasInitiallyScrolled) { if (convo.status === ConvoStatus.Ready && hasInitiallyScrolled) {
chat.fetchMessageHistory() convo.fetchMessageHistory()
} }
}, [chat, hasInitiallyScrolled]) }, [convo, hasInitiallyScrolled])
const onSendMessage = useCallback( const onSendMessage = useCallback(
(text: string) => { (text: string) => {
if (chat.status === ConvoStatus.Ready) { if (convo.status === ConvoStatus.Ready) {
chat.sendMessage({ convo.sendMessage({
text, text,
}) })
} }
}, },
[chat], [convo],
) )
const onScroll = React.useCallback( const onScroll = React.useCallback(
@ -229,7 +229,7 @@ export function MessagesList() {
<ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}> <ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}>
<List <List
ref={flatListRef} ref={flatListRef}
data={chat.items} data={convo.items}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
disableVirtualization={true} disableVirtualization={true}
@ -248,7 +248,7 @@ export function MessagesList() {
onScrollToIndexFailed={onScrollToIndexFailed} onScrollToIndexFailed={onScrollToIndexFailed}
scrollEventThrottle={100} scrollEventThrottle={100}
ListHeaderComponent={ ListHeaderComponent={
<MaybeLoader isLoading={chat.isFetchingHistory} /> <MaybeLoader isLoading={convo.isFetchingHistory} />
} }
/> />
</ScrollProvider> </ScrollProvider>

View File

@ -13,8 +13,8 @@ import {useGate} from '#/lib/statsig/statsig'
import {useCurrentConvoId} from '#/state/messages/current-convo-id' import {useCurrentConvoId} from '#/state/messages/current-convo-id'
import {BACK_HITSLOP} from 'lib/constants' import {BACK_HITSLOP} from 'lib/constants'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {ChatProvider, useChat} from 'state/messages' import {ConvoProvider, useConvo} from 'state/messages/convo'
import {ConvoStatus} from 'state/messages/convo' import {ConvoStatus} from 'state/messages/convo/types'
import {PreviewableUserAvatar} from 'view/com/util/UserAvatar' import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {MessagesList} from '#/screens/Messages/Conversation/MessagesList' import {MessagesList} from '#/screens/Messages/Conversation/MessagesList'
@ -46,23 +46,23 @@ export function MessagesConversationScreen({route}: Props) {
if (!gate('dms')) return <ClipClopGate /> if (!gate('dms')) return <ClipClopGate />
return ( return (
<ChatProvider convoId={convoId}> <ConvoProvider convoId={convoId}>
<Inner /> <Inner />
</ChatProvider> </ConvoProvider>
) )
} }
function Inner() { function Inner() {
const chat = useChat() const convo = useConvo()
if ( if (
chat.status === ConvoStatus.Uninitialized || convo.status === ConvoStatus.Uninitialized ||
chat.status === ConvoStatus.Initializing convo.status === ConvoStatus.Initializing
) { ) {
return <ListMaybePlaceholder isLoading /> return <ListMaybePlaceholder isLoading />
} }
if (chat.status === ConvoStatus.Error) { if (convo.status === ConvoStatus.Error) {
// TODO // TODO
return ( return (
<View> <View>
@ -71,7 +71,7 @@ function Inner() {
<Button <Button
label="Retry" label="Retry"
onPress={() => { onPress={() => {
chat.error.retry() convo.error.retry()
}}> }}>
<ButtonText>Retry</ButtonText> <ButtonText>Retry</ButtonText>
</Button> </Button>
@ -81,13 +81,13 @@ function Inner() {
} }
/* /*
* Any other chat states (atm) are "ready" states * Any other convo states (atm) are "ready" states
*/ */
return ( return (
<KeyboardProvider> <KeyboardProvider>
<CenteredView style={{flex: 1}} sideBorders> <CenteredView style={{flex: 1}} sideBorders>
<Header profile={chat.recipients[0]} /> <Header profile={convo.recipients[0]} />
<MessagesList /> <MessagesList />
</CenteredView> </CenteredView>
</KeyboardProvider> </KeyboardProvider>
@ -103,7 +103,7 @@ let Header = ({
const {_} = useLingui() const {_} = useLingui()
const {gtTablet} = useBreakpoints() const {gtTablet} = useBreakpoints()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const chat = useChat() const convo = useConvo()
const onPressBack = useCallback(() => { const onPressBack = useCallback(() => {
if (isWeb) { if (isWeb) {
@ -157,9 +157,9 @@ let Header = ({
{profile.displayName} {profile.displayName}
</Text> </Text>
</View> </View>
{chat.status === ConvoStatus.Ready ? ( {convo.status === ConvoStatus.Ready ? (
<ConvoMenu <ConvoMenu
convo={chat.convo} convo={convo.convo}
profile={profile} profile={profile}
onUpdateConvo={onUpdateConvo} onUpdateConvo={onUpdateConvo}
currentScreen="conversation" currentScreen="conversation"

View File

@ -9,178 +9,16 @@ import {nanoid} from 'nanoid/non-secure'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {
export type ConvoParams = { ConvoDispatch,
convoId: string ConvoDispatchEvent,
agent: BskyAgent ConvoErrorCode,
__tempFromUserDid: string ConvoItem,
} ConvoItemError,
ConvoParams,
export enum ConvoStatus { ConvoState,
Uninitialized = 'uninitialized', ConvoStatus,
Initializing = 'initializing', } from '#/state/messages/convo/types'
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
}
const ACTIVE_POLL_INTERVAL = 1e3 const ACTIVE_POLL_INTERVAL = 1e3
const BACKGROUND_POLL_INTERVAL = 10e3 const BACKGROUND_POLL_INTERVAL = 10e3
@ -235,7 +73,6 @@ export class Convo {
private headerItems: Map<string, ConvoItem> = new Map() private headerItems: Map<string, ConvoItem> = new Map()
private isProcessingPendingMessages = false private isProcessingPendingMessages = false
private pendingPoll: Promise<void> | undefined
private nextPoll: NodeJS.Timeout | undefined private nextPoll: NodeJS.Timeout | undefined
convoId: string convoId: string

View File

@ -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>
}

View File

@ -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
}

View File

@ -1,79 +1,7 @@
import React, {useContext, useState, useSyncExternalStore} from 'react' import React from 'react'
import {AppState} from 'react-native'
import {BskyAgent} from '@atproto-labs/api'
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
import {Convo, ConvoParams, ConvoState} from '#/state/messages/convo'
import {CurrentConvoIdProvider} from '#/state/messages/current-convo-id' import {CurrentConvoIdProvider} from '#/state/messages/current-convo-id'
import {MessagesEventBusProvider} from '#/state/messages/events' 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}) { export function MessagesProvider({children}: {children: React.ReactNode}) {
return ( return (