diff options
Diffstat (limited to 'src/state')
-rw-r--r-- | src/state/messages/convo/agent.ts | 192 | ||||
-rw-r--r-- | src/state/messages/convo/index.tsx | 14 | ||||
-rw-r--r-- | src/state/messages/convo/types.ts | 26 | ||||
-rw-r--r-- | src/state/queries/messages/list-conversations.tsx | 67 |
4 files changed, 263 insertions, 36 deletions
diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts index f6a8d6dc4..909213975 100644 --- a/src/state/messages/convo/agent.ts +++ b/src/state/messages/convo/agent.ts @@ -1,9 +1,9 @@ import { - BskyAgent, - ChatBskyActorDefs, + type AtpAgent, + type ChatBskyActorDefs, ChatBskyConvoDefs, - ChatBskyConvoGetLog, - ChatBskyConvoSendMessage, + type ChatBskyConvoGetLog, + type ChatBskyConvoSendMessage, } from '@atproto/api' import {XRPCError} from '@atproto/xrpc' import EventEmitter from 'eventemitter3' @@ -19,19 +19,19 @@ import { NETWORK_FAILURE_STATUSES, } from '#/state/messages/convo/const' import { - ConvoDispatch, + type ConvoDispatch, ConvoDispatchEvent, - ConvoError, + type ConvoError, ConvoErrorCode, - ConvoEvent, - ConvoItem, + type ConvoEvent, + type ConvoItem, ConvoItemError, - ConvoParams, - ConvoState, + type ConvoParams, + type ConvoState, ConvoStatus, } from '#/state/messages/convo/types' -import {MessagesEventBus} from '#/state/messages/events/agent' -import {MessagesEventBusError} from '#/state/messages/events/types' +import {type MessagesEventBus} from '#/state/messages/events/agent' +import {type MessagesEventBusError} from '#/state/messages/events/types' import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const' const logger = Logger.create(Logger.Context.ConversationAgent) @@ -50,7 +50,7 @@ export function isConvoItemMessage( export class Convo { private id: string - private agent: BskyAgent + private agent: AtpAgent private events: MessagesEventBus private senderUserDid: string @@ -106,6 +106,8 @@ export class Convo { this.onFirehoseConnect = this.onFirehoseConnect.bind(this) this.onFirehoseError = this.onFirehoseError.bind(this) this.markConvoAccepted = this.markConvoAccepted.bind(this) + this.addReaction = this.addReaction.bind(this) + this.removeReaction = this.removeReaction.bind(this) } private commit() { @@ -147,6 +149,8 @@ export class Convo { sendMessage: undefined, fetchMessageHistory: undefined, markConvoAccepted: undefined, + addReaction: undefined, + removeReaction: undefined, } } case ConvoStatus.Disabled: @@ -165,6 +169,8 @@ export class Convo { sendMessage: this.sendMessage, fetchMessageHistory: this.fetchMessageHistory, markConvoAccepted: this.markConvoAccepted, + addReaction: this.addReaction, + removeReaction: this.removeReaction, } } case ConvoStatus.Error: { @@ -180,6 +186,8 @@ export class Convo { sendMessage: undefined, fetchMessageHistory: undefined, markConvoAccepted: undefined, + addReaction: undefined, + removeReaction: undefined, } } default: { @@ -195,6 +203,8 @@ export class Convo { sendMessage: undefined, fetchMessageHistory: undefined, markConvoAccepted: undefined, + addReaction: undefined, + removeReaction: undefined, } } } @@ -760,6 +770,22 @@ export class Convo { this.deletedMessages.delete(ev.message.id) needsCommit = true } + } else if ( + (ChatBskyConvoDefs.isLogAddReaction(ev) || + ChatBskyConvoDefs.isLogRemoveReaction(ev)) && + ChatBskyConvoDefs.isMessageView(ev.message) + ) { + /* + * Update if we have this in state - replace message wholesale. If we don't, don't worry about it. + */ + if (this.pastMessages.has(ev.message.id)) { + this.pastMessages.set(ev.message.id, ev.message) + needsCommit = true + } + if (this.newMessages.has(ev.message.id)) { + this.newMessages.set(ev.message.id, ev.message) + needsCommit = true + } } } } @@ -1141,4 +1167,144 @@ export class Convo { return item }) } + + /** + * Add an emoji reaction to a message + * + * @param messageId - the id of the message to add the reaction to + * @param emoji - must be one grapheme + */ + async addReaction(messageId: string, emoji: string) { + const optimisticReaction = { + value: emoji, + sender: {did: this.senderUserDid}, + createdAt: new Date().toISOString(), + } + let restore: null | (() => void) = null + if (this.pastMessages.has(messageId)) { + const prevMessage = this.pastMessages.get(messageId) + if ( + ChatBskyConvoDefs.isMessageView(prevMessage) && + // skip optimistic update if reaction already exists + !prevMessage.reactions?.find( + reaction => + reaction.sender.did === this.senderUserDid && + reaction.value === emoji, + ) + ) { + if (prevMessage.reactions) { + if ( + prevMessage.reactions.filter( + reaction => reaction.sender.did === this.senderUserDid, + ).length >= 5 + ) { + throw new Error('Maximum reactions reached') + } + } + this.pastMessages.set(messageId, { + ...prevMessage, + reactions: [...(prevMessage.reactions ?? []), optimisticReaction], + }) + this.commit() + restore = () => { + this.pastMessages.set(messageId, prevMessage) + this.commit() + } + } + } else if (this.newMessages.has(messageId)) { + const prevMessage = this.newMessages.get(messageId) + if ( + ChatBskyConvoDefs.isMessageView(prevMessage) && + !prevMessage.reactions?.find(reaction => reaction.value === emoji) + ) { + if (prevMessage.reactions && prevMessage.reactions.length >= 5) + throw new Error('Maximum reactions reached') + this.newMessages.set(messageId, { + ...prevMessage, + reactions: [...(prevMessage.reactions ?? []), optimisticReaction], + }) + this.commit() + restore = () => { + this.newMessages.set(messageId, prevMessage) + this.commit() + } + } + } + + try { + logger.info(`Adding reaction ${emoji} to message ${messageId}`) + const {data} = await this.agent.chat.bsky.convo.addReaction( + {messageId, value: emoji, convoId: this.convoId}, + {encoding: 'application/json', headers: DM_SERVICE_HEADERS}, + ) + if (ChatBskyConvoDefs.isMessageView(data.message)) { + if (this.pastMessages.has(messageId)) { + this.pastMessages.set(messageId, data.message) + this.commit() + } else if (this.newMessages.has(messageId)) { + this.newMessages.set(messageId, data.message) + this.commit() + } + } + } catch (error) { + if (restore) restore() + throw error + } + } + + /* + * Remove a reaction from a message. + * + * @param messageId - The ID of the message to remove the reaction from. + * @param emoji - The emoji to remove. + */ + async removeReaction(messageId: string, emoji: string) { + let restore: null | (() => void) = null + if (this.pastMessages.has(messageId)) { + const prevMessage = this.pastMessages.get(messageId) + if (ChatBskyConvoDefs.isMessageView(prevMessage)) { + this.pastMessages.set(messageId, { + ...prevMessage, + reactions: prevMessage.reactions?.filter( + reaction => + reaction.value !== emoji || + reaction.sender.did !== this.senderUserDid, + ), + }) + this.commit() + restore = () => { + this.pastMessages.set(messageId, prevMessage) + this.commit() + } + } + } else if (this.newMessages.has(messageId)) { + const prevMessage = this.newMessages.get(messageId) + if (ChatBskyConvoDefs.isMessageView(prevMessage)) { + this.newMessages.set(messageId, { + ...prevMessage, + reactions: prevMessage.reactions?.filter( + reaction => + reaction.value !== emoji || + reaction.sender.did !== this.senderUserDid, + ), + }) + this.commit() + restore = () => { + this.newMessages.set(messageId, prevMessage) + this.commit() + } + } + } + + try { + logger.info(`Removing reaction ${emoji} from message ${messageId}`) + await this.agent.chat.bsky.convo.removeReaction( + {messageId, value: emoji, convoId: this.convoId}, + {encoding: 'application/json', headers: DM_SERVICE_HEADERS}, + ) + } catch (error) { + if (restore) restore() + throw error + } + } } diff --git a/src/state/messages/convo/index.tsx b/src/state/messages/convo/index.tsx index f004566e8..a53f08900 100644 --- a/src/state/messages/convo/index.tsx +++ b/src/state/messages/convo/index.tsx @@ -1,17 +1,17 @@ import React, {useContext, useState, useSyncExternalStore} from 'react' -import {ChatBskyConvoDefs} from '@atproto/api' +import {type ChatBskyConvoDefs} from '@atproto/api' import {useFocusEffect} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' import {useAppState} from '#/lib/hooks/useAppState' import {Convo} from '#/state/messages/convo/agent' import { - ConvoParams, - ConvoState, - ConvoStateBackgrounded, - ConvoStateDisabled, - ConvoStateReady, - ConvoStateSuspended, + type ConvoParams, + type ConvoState, + type ConvoStateBackgrounded, + type ConvoStateDisabled, + type ConvoStateReady, + type ConvoStateSuspended, } from '#/state/messages/convo/types' import {isConvoActive} from '#/state/messages/convo/util' import {useMessagesEventBus} from '#/state/messages/events' diff --git a/src/state/messages/convo/types.ts b/src/state/messages/convo/types.ts index 83499de2e..705387793 100644 --- a/src/state/messages/convo/types.ts +++ b/src/state/messages/convo/types.ts @@ -1,11 +1,11 @@ import { - BskyAgent, - ChatBskyActorDefs, - ChatBskyConvoDefs, - ChatBskyConvoSendMessage, + type BskyAgent, + type ChatBskyActorDefs, + type ChatBskyConvoDefs, + type ChatBskyConvoSendMessage, } from '@atproto/api' -import {MessagesEventBus} from '#/state/messages/events/agent' +import {type MessagesEventBus} from '#/state/messages/events/agent' export type ConvoParams = { convoId: string @@ -142,6 +142,8 @@ type SendMessage = ( ) => void type FetchMessageHistory = () => Promise<void> type MarkConvoAccepted = () => void +type AddReaction = (messageId: string, reaction: string) => Promise<void> +type RemoveReaction = (messageId: string, reaction: string) => Promise<void> export type ConvoStateUninitialized = { status: ConvoStatus.Uninitialized @@ -155,6 +157,8 @@ export type ConvoStateUninitialized = { sendMessage: undefined fetchMessageHistory: undefined markConvoAccepted: undefined + addReaction: undefined + removeReaction: undefined } export type ConvoStateInitializing = { status: ConvoStatus.Initializing @@ -168,6 +172,8 @@ export type ConvoStateInitializing = { sendMessage: undefined fetchMessageHistory: undefined markConvoAccepted: undefined + addReaction: undefined + removeReaction: undefined } export type ConvoStateReady = { status: ConvoStatus.Ready @@ -181,6 +187,8 @@ export type ConvoStateReady = { sendMessage: SendMessage fetchMessageHistory: FetchMessageHistory markConvoAccepted: MarkConvoAccepted + addReaction: AddReaction + removeReaction: RemoveReaction } export type ConvoStateBackgrounded = { status: ConvoStatus.Backgrounded @@ -194,6 +202,8 @@ export type ConvoStateBackgrounded = { sendMessage: SendMessage fetchMessageHistory: FetchMessageHistory markConvoAccepted: MarkConvoAccepted + addReaction: AddReaction + removeReaction: RemoveReaction } export type ConvoStateSuspended = { status: ConvoStatus.Suspended @@ -207,6 +217,8 @@ export type ConvoStateSuspended = { sendMessage: SendMessage fetchMessageHistory: FetchMessageHistory markConvoAccepted: MarkConvoAccepted + addReaction: AddReaction + removeReaction: RemoveReaction } export type ConvoStateError = { status: ConvoStatus.Error @@ -220,6 +232,8 @@ export type ConvoStateError = { sendMessage: undefined fetchMessageHistory: undefined markConvoAccepted: undefined + addReaction: undefined + removeReaction: undefined } export type ConvoStateDisabled = { status: ConvoStatus.Disabled @@ -233,6 +247,8 @@ export type ConvoStateDisabled = { sendMessage: SendMessage fetchMessageHistory: FetchMessageHistory markConvoAccepted: MarkConvoAccepted + addReaction: AddReaction + removeReaction: RemoveReaction } export type ConvoState = | ConvoStateUninitialized diff --git a/src/state/queries/messages/list-conversations.tsx b/src/state/queries/messages/list-conversations.tsx index f5fce6347..066c25e21 100644 --- a/src/state/queries/messages/list-conversations.tsx +++ b/src/state/queries/messages/list-conversations.tsx @@ -1,19 +1,13 @@ -import React, { - createContext, - useCallback, - useContext, - useEffect, - useMemo, -} from 'react' +import {createContext, useCallback, useContext, useEffect, useMemo} from 'react' import { ChatBskyConvoDefs, - ChatBskyConvoListConvos, + type ChatBskyConvoListConvos, moderateProfile, - ModerationOpts, + type ModerationOpts, } from '@atproto/api' import { - InfiniteData, - QueryClient, + type InfiniteData, + type QueryClient, useInfiniteQuery, useQueryClient, } from '@tanstack/react-query' @@ -316,6 +310,57 @@ export function ListConvosProviderInner({ rev: logRef.rev, })), ) + } else if (ChatBskyConvoDefs.isLogAddReaction(log)) { + const logRef: ChatBskyConvoDefs.LogAddReaction = log + queryClient.setQueriesData( + {queryKey: [RQKEY_ROOT]}, + (old?: ConvoListQueryData) => + optimisticUpdate(logRef.convoId, old, convo => ({ + ...convo, + lastMessage: { + $type: 'chat.bsky.convo.defs#messageAndReactionView', + reaction: logRef.reaction, + message: logRef.message, + }, + rev: logRef.rev, + })), + ) + } else if (ChatBskyConvoDefs.isLogRemoveReaction(log)) { + if (ChatBskyConvoDefs.isMessageView(log.message)) { + for (const [_queryKey, queryData] of queryClient.getQueriesData< + InfiniteData<ChatBskyConvoListConvos.OutputSchema> + >({ + queryKey: [RQKEY_ROOT], + })) { + if (!queryData?.pages) { + continue + } + + for (const page of queryData.pages) { + for (const convo of page.convos) { + if ( + // if the convo is the same + log.convoId === convo.id && + ChatBskyConvoDefs.isMessageAndReactionView( + convo.lastMessage, + ) && + ChatBskyConvoDefs.isMessageView( + convo.lastMessage.message, + ) && + // ...and the message is the same + convo.lastMessage.message.id === log.message.id && + // ...and the reaction is the same + convo.lastMessage.reaction.sender.did === + log.reaction.sender.did && + convo.lastMessage.reaction.value === log.reaction.value + ) { + // refetch, because we don't know what the last message is now + debouncedRefetch() + } + } + } + } + } } } }, |