[DMs] Reactions - link up API (attempt 2) (#8074)
* update package * wire up APIs * get reactions to display * allow removing emoji * handle limits better * listen to reactions in log * update convo list with reactions * tweaks to reaction display * Handle empty message fallback case * update package * shift reacts up by 2px --------- Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user