parent
814ec2bd7f
commit
56f713077f
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
|
@ -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>
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
|
Loading…
Reference in New Issue