move mark as read to convo state

This commit is contained in:
Samuel Newman
2025-01-06 15:08:34 +00:00
parent fe8d3b081c
commit fa34d92da0
6 changed files with 153 additions and 29 deletions
@@ -0,0 +1,75 @@
import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {Divider} from '#/components/Divider'
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt'
import {Text} from '#/components/Typography'
export function MessagesListPendingAcceptFooter({
// recipient: initialRecipient,
convoId,
hasMessages,
}: {
recipient: AppBskyActorDefs.ProfileViewBasic
convoId: string
hasMessages: boolean
}) {
const t = useTheme()
const {_} = useLingui()
// const recipient = useProfileShadow(initialRecipient)
const leaveConvoControl = useDialogControl()
const reportControl = useDialogControl()
return (
<View style={[hasMessages && a.pt_md, a.pb_xl, a.gap_lg]}>
<Divider />
<Text style={[a.text_md, a.font_bold, a.text_center]}>
{/* {isBlocking ? (
<Trans>You have blocked this user</Trans>
) : (
<Trans>This user has blocked you</Trans>
)} */}
</Text>
<View style={[a.flex_row, a.justify_between, a.gap_lg, a.px_md]}>
<Button
label={_(msg`Leave chat`)}
color="secondary"
variant="solid"
size="small"
style={[a.flex_1]}
onPress={leaveConvoControl.open}>
<ButtonText style={{color: t.palette.negative_500}}>
<Trans>Leave chat</Trans>
</ButtonText>
</Button>
<Button
label={_(msg`Report`)}
color="secondary"
variant="solid"
size="small"
style={[a.flex_1]}
onPress={reportControl.open}>
<ButtonText style={{color: t.palette.negative_500}}>
<Trans>Report</Trans>
</ButtonText>
</Button>
</View>
<LeaveConvoPrompt
control={leaveConvoControl}
currentScreen="conversation"
convoId={convoId}
/>
<ReportConversationPrompt control={reportControl} />
</View>
)
}
+18 -7
View File
@@ -23,9 +23,11 @@ import {useDialogControl} from '#/components/Dialog'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter' import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter'
import {MessagesListHeader} from '#/components/dms/MessagesListHeader' import {MessagesListHeader} from '#/components/dms/MessagesListHeader'
import {MessagesListPendingAcceptFooter} from '#/components/dms/MessagesListPendingAcceptFooter'
import {Error} from '#/components/Error' import {Error} from '#/components/Error'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {ChatDisabled} from './components/ChatDisabled'
type Props = NativeStackScreenProps< type Props = NativeStackScreenProps<
CommonNavigatorParams, CommonNavigatorParams,
@@ -193,14 +195,23 @@ function InnerReady({
<MessagesList <MessagesList
hasScrolled={hasScrolled} hasScrolled={hasScrolled}
setHasScrolled={setHasScrolled} setHasScrolled={setHasScrolled}
blocked={moderation?.blocked}
footer={ footer={
<MessagesListBlockedFooter convoState.status === ConvoStatus.Disabled ? (
recipient={recipient} <ChatDisabled />
convoId={convoState.convo.id} ) : moderation?.blocked ? (
hasMessages={convoState.items.length > 0} <MessagesListBlockedFooter
blockInfo={blockInfo} recipient={recipient}
/> convoId={convoState.convo.id}
hasMessages={convoState.items.length > 0}
blockInfo={blockInfo}
/>
) : convoState.pendingAcceptance ? (
<MessagesListPendingAcceptFooter
recipient={recipient}
convoId={convoState.convo.id}
hasMessages={convoState.items.length > 0}
/>
) : null
} }
/> />
)} )}
@@ -31,7 +31,6 @@ import {
EmojiPickerState, EmojiPickerState,
} from '#/view/com/composer/text-input/web/EmojiPicker.web' } from '#/view/com/composer/text-input/web/EmojiPicker.web'
import {List, ListMethods} from '#/view/com/util/List' import {List, ListMethods} from '#/view/com/util/List'
import {ChatDisabled} from '#/screens/Messages/components/ChatDisabled'
import {MessageInput} from '#/screens/Messages/components/MessageInput' import {MessageInput} from '#/screens/Messages/components/MessageInput'
import {MessageListError} from '#/screens/Messages/components/MessageListError' import {MessageListError} from '#/screens/Messages/components/MessageListError'
import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill' import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill'
@@ -78,13 +77,11 @@ function onScrollToIndexFailed() {
export function MessagesList({ export function MessagesList({
hasScrolled, hasScrolled,
setHasScrolled, setHasScrolled,
blocked,
footer, footer,
}: { }: {
hasScrolled: boolean hasScrolled: boolean
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>> setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
blocked?: boolean footer: React.ReactNode | null
footer?: React.ReactNode
}) { }) {
const convoState = useConvoActive() const convoState = useConvoActive()
const agent = useAgent() const agent = useAgent()
@@ -427,11 +424,7 @@ export function MessagesList({
/> />
</ScrollProvider> </ScrollProvider>
<Animated.View style={animatedStickyViewStyle}> <Animated.View style={animatedStickyViewStyle}>
{convoState.status === ConvoStatus.Disabled ? ( {footer || (
<ChatDisabled />
) : blocked ? (
footer
) : (
<> <>
{isConvoActive(convoState) && {isConvoActive(convoState) &&
!convoState.isFetchingHistory && !convoState.isFetchingHistory &&
+39 -7
View File
@@ -78,10 +78,13 @@ export class Convo {
private emitter = new EventEmitter<{event: [ConvoEvent]}>() private emitter = new EventEmitter<{event: [ConvoEvent]}>()
private onMarkAsRead: () => void
convoId: string convoId: string
convo: ChatBskyConvoDefs.ConvoView | undefined convo: ChatBskyConvoDefs.ConvoView | undefined
sender: AppBskyActorDefs.ProfileViewBasic | undefined sender: AppBskyActorDefs.ProfileViewBasic | undefined
recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined = undefined recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined = undefined
pendingAcceptance: boolean | undefined
snapshot: ConvoState | undefined snapshot: ConvoState | undefined
constructor(params: ConvoParams) { constructor(params: ConvoParams) {
@@ -89,7 +92,8 @@ export class Convo {
this.convoId = params.convoId this.convoId = params.convoId
this.agent = params.agent this.agent = params.agent
this.events = params.events this.events = params.events
this.senderUserDid = params.agent.session?.did! this.senderUserDid = params.agent.session!.did
this.onMarkAsRead = params.onMarkAsRead
this.subscribe = this.subscribe.bind(this) this.subscribe = this.subscribe.bind(this)
this.getSnapshot = this.getSnapshot.bind(this) this.getSnapshot = this.getSnapshot.bind(this)
@@ -139,6 +143,7 @@ export class Convo {
deleteMessage: undefined, deleteMessage: undefined,
sendMessage: undefined, sendMessage: undefined,
fetchMessageHistory: undefined, fetchMessageHistory: undefined,
pendingAcceptance: undefined,
} }
} }
case ConvoStatus.Disabled: case ConvoStatus.Disabled:
@@ -156,6 +161,7 @@ export class Convo {
deleteMessage: this.deleteMessage, deleteMessage: this.deleteMessage,
sendMessage: this.sendMessage, sendMessage: this.sendMessage,
fetchMessageHistory: this.fetchMessageHistory, fetchMessageHistory: this.fetchMessageHistory,
pendingAcceptance: this.pendingAcceptance,
} }
} }
case ConvoStatus.Error: { case ConvoStatus.Error: {
@@ -170,6 +176,7 @@ export class Convo {
deleteMessage: undefined, deleteMessage: undefined,
sendMessage: undefined, sendMessage: undefined,
fetchMessageHistory: undefined, fetchMessageHistory: undefined,
pendingAcceptance: undefined,
} }
} }
default: { default: {
@@ -184,6 +191,7 @@ export class Convo {
deleteMessage: undefined, deleteMessage: undefined,
sendMessage: undefined, sendMessage: undefined,
fetchMessageHistory: undefined, fetchMessageHistory: undefined,
pendingAcceptance: undefined,
} }
} }
} }
@@ -246,11 +254,13 @@ export class Convo {
case ConvoDispatchEvent.Resume: { case ConvoDispatchEvent.Resume: {
this.refreshConvo() this.refreshConvo()
this.requestPollInterval(ACTIVE_POLL_INTERVAL) this.requestPollInterval(ACTIVE_POLL_INTERVAL)
this.markAsRead()
break break
} }
case ConvoDispatchEvent.Background: { case ConvoDispatchEvent.Background: {
this.status = ConvoStatus.Backgrounded this.status = ConvoStatus.Backgrounded
this.requestPollInterval(BACKGROUND_POLL_INTERVAL) this.requestPollInterval(BACKGROUND_POLL_INTERVAL)
this.markAsRead()
break break
} }
case ConvoDispatchEvent.Suspend: { case ConvoDispatchEvent.Suspend: {
@@ -513,6 +523,7 @@ export class Convo {
convo: ChatBskyConvoDefs.ConvoView convo: ChatBskyConvoDefs.ConvoView
sender: AppBskyActorDefs.ProfileViewBasic | undefined sender: AppBskyActorDefs.ProfileViewBasic | undefined
recipients: AppBskyActorDefs.ProfileViewBasic[] recipients: AppBskyActorDefs.ProfileViewBasic[]
pendingAcceptance: boolean
}> }>
| undefined | undefined
async fetchConvo() { async fetchConvo() {
@@ -522,10 +533,11 @@ export class Convo {
convo: ChatBskyConvoDefs.ConvoView convo: ChatBskyConvoDefs.ConvoView
sender: AppBskyActorDefs.ProfileViewBasic | undefined sender: AppBskyActorDefs.ProfileViewBasic | undefined
recipients: AppBskyActorDefs.ProfileViewBasic[] recipients: AppBskyActorDefs.ProfileViewBasic[]
pendingAcceptance: boolean
}>(async (resolve, reject) => { }>(async (resolve, reject) => {
try { try {
const response = await networkRetry(2, () => { const response = await networkRetry(2, () => {
return this.agent.api.chat.bsky.convo.getConvo( return this.agent.chat.bsky.convo.getConvo(
{ {
convoId: this.convoId, convoId: this.convoId,
}, },
@@ -539,6 +551,7 @@ export class Convo {
convo, convo,
sender: convo.members.find(m => m.did === this.senderUserDid), sender: convo.members.find(m => m.did === this.senderUserDid),
recipients: convo.members.filter(m => m.did !== this.senderUserDid), recipients: convo.members.filter(m => m.did !== this.senderUserDid),
pendingAcceptance: !convo.opened,
}) })
} catch (e) { } catch (e) {
reject(e) reject(e)
@@ -552,11 +565,13 @@ export class Convo {
async refreshConvo() { async refreshConvo() {
try { try {
const {convo, sender, recipients} = await this.fetchConvo() const {convo, sender, recipients, pendingAcceptance} =
await this.fetchConvo()
// throw new Error('UNCOMMENT TO TEST REFRESH FAILURE') // throw new Error('UNCOMMENT TO TEST REFRESH FAILURE')
this.convo = convo || this.convo this.convo = convo || this.convo
this.sender = sender || this.sender this.sender = sender || this.sender
this.recipients = recipients || this.recipients this.recipients = recipients || this.recipients
this.pendingAcceptance = pendingAcceptance ?? this.pendingAcceptance
} catch (e: any) { } catch (e: any) {
logger.error(e, {context: `Convo: failed to refresh convo`}) logger.error(e, {context: `Convo: failed to refresh convo`})
} }
@@ -592,7 +607,7 @@ export class Convo {
const nextCursor = this.oldestRev // for TS const nextCursor = this.oldestRev // for TS
const response = await networkRetry(2, () => { const response = await networkRetry(2, () => {
return this.agent.api.chat.bsky.convo.getMessages( return this.agent.chat.bsky.convo.getMessages(
{ {
cursor: nextCursor, cursor: nextCursor,
convoId: this.convoId, convoId: this.convoId,
@@ -793,7 +808,7 @@ export class Convo {
const {id, message} = pendingMessage const {id, message} = pendingMessage
const response = await this.agent.api.chat.bsky.convo.sendMessage( const response = await this.agent.chat.bsky.convo.sendMessage(
{ {
convoId: this.convoId, convoId: this.convoId,
message, message,
@@ -888,7 +903,7 @@ export class Convo {
) )
try { try {
const {data} = await this.agent.api.chat.bsky.convo.sendMessageBatch( const {data} = await this.agent.chat.bsky.convo.sendMessageBatch(
{ {
items: messageArray.map(({message}) => ({ items: messageArray.map(({message}) => ({
convoId: this.convoId, convoId: this.convoId,
@@ -935,7 +950,7 @@ export class Convo {
try { try {
await networkRetry(2, () => { await networkRetry(2, () => {
return this.agent.api.chat.bsky.convo.deleteMessageForSelf( return this.agent.chat.bsky.convo.deleteMessageForSelf(
{ {
convoId: this.convoId, convoId: this.convoId,
messageId, messageId,
@@ -1104,4 +1119,21 @@ export class Convo {
return item return item
}) })
} }
async acceptChat() {
await this.agent.chat.bsky.convo.updateRead({
convoId: this.convoId,
})
this.pendingAcceptance = false
this.onMarkAsRead()
}
markAsRead() {
if (!this.pendingAcceptance) {
this.agent.chat.bsky.convo.updateRead({
convoId: this.convoId,
})
this.onMarkAsRead()
}
}
} }
+11 -6
View File
@@ -3,6 +3,7 @@ import {useFocusEffect} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {useAppState} from '#/lib/hooks/useAppState' import {useAppState} from '#/lib/hooks/useAppState'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {Convo} from '#/state/messages/convo/agent' import {Convo} from '#/state/messages/convo/agent'
import { import {
ConvoParams, ConvoParams,
@@ -14,8 +15,10 @@ import {
} from '#/state/messages/convo/types' } from '#/state/messages/convo/types'
import {isConvoActive} from '#/state/messages/convo/util' import {isConvoActive} from '#/state/messages/convo/util'
import {useMessagesEventBus} from '#/state/messages/events' import {useMessagesEventBus} from '#/state/messages/events'
import {useMarkAsReadMutation} from '#/state/queries/messages/conversation' import {
import {RQKEY as ListConvosQueryKey} from '#/state/queries/messages/list-conversations' RQKEY as ListConvosQueryKey,
useOnMarkAsRead,
} from '#/state/queries/messages/list-conversations'
import {RQKEY as createProfileQueryKey} from '#/state/queries/profile' import {RQKEY as createProfileQueryKey} from '#/state/queries/profile'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
@@ -60,16 +63,20 @@ export function ConvoProvider({
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const agent = useAgent()
const events = useMessagesEventBus() const events = useMessagesEventBus()
const onMarkAsRead = useOnMarkAsRead()
const stableMAR = useNonReactiveCallback(() => {
onMarkAsRead(convoId)
})
const [convo] = useState( const [convo] = useState(
() => () =>
new Convo({ new Convo({
convoId, convoId,
agent, agent,
events, events,
onMarkAsRead: stableMAR,
}), }),
) )
const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot) const service = useSyncExternalStore(convo.subscribe, convo.getSnapshot)
const {mutate: markAsRead} = useMarkAsReadMutation()
const appState = useAppState() const appState = useAppState()
const isActive = appState === 'active' const isActive = appState === 'active'
@@ -77,14 +84,12 @@ export function ConvoProvider({
React.useCallback(() => { React.useCallback(() => {
if (isActive) { if (isActive) {
convo.resume() convo.resume()
markAsRead({convoId})
return () => { return () => {
convo.background() convo.background()
markAsRead({convoId})
} }
} }
}, [isActive, convo, convoId, markAsRead]), }, [isActive, convo]),
) )
React.useEffect(() => { React.useEffect(() => {
+8
View File
@@ -11,6 +11,7 @@ export type ConvoParams = {
convoId: string convoId: string
agent: BskyAgent agent: BskyAgent
events: MessagesEventBus events: MessagesEventBus
onMarkAsRead: () => void
} }
export enum ConvoStatus { export enum ConvoStatus {
@@ -150,6 +151,7 @@ export type ConvoStateUninitialized = {
deleteMessage: undefined deleteMessage: undefined
sendMessage: undefined sendMessage: undefined
fetchMessageHistory: undefined fetchMessageHistory: undefined
pendingAcceptance: undefined
} }
export type ConvoStateInitializing = { export type ConvoStateInitializing = {
status: ConvoStatus.Initializing status: ConvoStatus.Initializing
@@ -162,6 +164,7 @@ export type ConvoStateInitializing = {
deleteMessage: undefined deleteMessage: undefined
sendMessage: undefined sendMessage: undefined
fetchMessageHistory: undefined fetchMessageHistory: undefined
pendingAcceptance: undefined
} }
export type ConvoStateReady = { export type ConvoStateReady = {
status: ConvoStatus.Ready status: ConvoStatus.Ready
@@ -174,6 +177,7 @@ export type ConvoStateReady = {
deleteMessage: DeleteMessage deleteMessage: DeleteMessage
sendMessage: SendMessage sendMessage: SendMessage
fetchMessageHistory: FetchMessageHistory fetchMessageHistory: FetchMessageHistory
pendingAcceptance: boolean | undefined
} }
export type ConvoStateBackgrounded = { export type ConvoStateBackgrounded = {
status: ConvoStatus.Backgrounded status: ConvoStatus.Backgrounded
@@ -186,6 +190,7 @@ export type ConvoStateBackgrounded = {
deleteMessage: DeleteMessage deleteMessage: DeleteMessage
sendMessage: SendMessage sendMessage: SendMessage
fetchMessageHistory: FetchMessageHistory fetchMessageHistory: FetchMessageHistory
pendingAcceptance: boolean | undefined
} }
export type ConvoStateSuspended = { export type ConvoStateSuspended = {
status: ConvoStatus.Suspended status: ConvoStatus.Suspended
@@ -198,6 +203,7 @@ export type ConvoStateSuspended = {
deleteMessage: DeleteMessage deleteMessage: DeleteMessage
sendMessage: SendMessage sendMessage: SendMessage
fetchMessageHistory: FetchMessageHistory fetchMessageHistory: FetchMessageHistory
pendingAcceptance: boolean | undefined
} }
export type ConvoStateError = { export type ConvoStateError = {
status: ConvoStatus.Error status: ConvoStatus.Error
@@ -210,6 +216,7 @@ export type ConvoStateError = {
deleteMessage: undefined deleteMessage: undefined
sendMessage: undefined sendMessage: undefined
fetchMessageHistory: undefined fetchMessageHistory: undefined
pendingAcceptance: undefined
} }
export type ConvoStateDisabled = { export type ConvoStateDisabled = {
status: ConvoStatus.Disabled status: ConvoStatus.Disabled
@@ -222,6 +229,7 @@ export type ConvoStateDisabled = {
deleteMessage: DeleteMessage deleteMessage: DeleteMessage
sendMessage: SendMessage sendMessage: SendMessage
fetchMessageHistory: FetchMessageHistory fetchMessageHistory: FetchMessageHistory
pendingAcceptance: boolean | undefined
} }
export type ConvoState = export type ConvoState =
| ConvoStateUninitialized | ConvoStateUninitialized