diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index bfb0f1b545..25a38793ea 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -2,6 +2,7 @@ import React, { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useS import { KeyboardAvoidingViewProps, StyleSheet, Text, View } from 'react-native'; import debounce from 'lodash/debounce'; +import omit from 'lodash/omit'; import throttle from 'lodash/throttle'; import { lookup } from 'mime-types'; @@ -1631,40 +1632,28 @@ const ChannelWithContext = < ) => { try { const updatedMessage = await uploadPendingAttachments(message); - const { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - __html, - attachments, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - created_at, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - deleted_at, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - html, - id, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - latest_reactions, - mentioned_users, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - own_reactions, - parent_id, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - quoted_message, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - reaction_counts, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - reactions, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - status, - text, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - updated_at, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - user, - ...extraFields - } = updatedMessage; + const extraFields = omit(updatedMessage, [ + '__html', + 'attachments', + 'created_at', + 'deleted_at', + 'html', + 'id', + 'latest_reactions', + 'mentioned_users', + 'own_reactions', + 'parent_id', + 'quoted_message', + 'reaction_counts', + 'reaction_groups', + 'reactions', + 'status', + 'text', + 'type', + 'updated_at', + 'user', + ]); + const { attachments, id, mentioned_users, parent_id, text } = updatedMessage; if (!channel.id) return; const mentionedUserIds = mentioned_users?.map((user) => user.id) || []; @@ -2000,6 +1989,7 @@ const ChannelWithContext = < }, }); }; + const deleteMessage: MessagesContextValue['deleteMessage'] = async ( message, ) => { diff --git a/package/src/components/Chat/hooks/handleEventToSyncDB.ts b/package/src/components/Chat/hooks/handleEventToSyncDB.ts index f53357c422..d93bf5715f 100644 --- a/package/src/components/Chat/hooks/handleEventToSyncDB.ts +++ b/package/src/components/Chat/hooks/handleEventToSyncDB.ts @@ -124,8 +124,7 @@ export const handleEventToSyncDB = < if (type === 'reaction.updated') { const message = event.message; if (message && event.reaction) { - // We update the entire message to make sure we also update - // reaction_counts. + // We update the entire message to make sure we also update reaction_groups return queriesWithChannelGuard((flushOverride) => updateMessage({ flush: flushOverride, diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index 3f08e89cad..866daaf44c 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -6,6 +6,7 @@ import type { Attachment, UserResponse } from 'stream-chat'; import { useCreateMessageContext } from './hooks/useCreateMessageContext'; import { useMessageActionHandlers } from './hooks/useMessageActionHandlers'; import { useMessageActions } from './hooks/useMessageActions'; +import { useProcessReactions } from './hooks/useProcessReactions'; import { messageActions as defaultMessageActions } from './utils/messageActions'; import { @@ -17,11 +18,7 @@ import { KeyboardContextValue, useKeyboardContext, } from '../../contexts/keyboardContext/KeyboardContext'; -import { - MessageContextValue, - MessageProvider, - Reactions, -} from '../../contexts/messageContext/MessageContext'; +import { MessageContextValue, MessageProvider } from '../../contexts/messageContext/MessageContext'; import { MessageOverlayContextValue, useMessageOverlayContext, @@ -468,28 +465,13 @@ const MessageWithContext = < } }; - const hasReactions = - !isMessageTypeDeleted && !!message.latest_reactions && message.latest_reactions.length > 0; - - const clientId = client.userID; - - const reactions = hasReactions - ? supportedReactions.reduce((acc, cur) => { - const reactionType = cur.type; - const reactionsOfReactionType = message.latest_reactions?.filter( - (reaction) => reaction.type === reactionType, - ); - - if (reactionsOfReactionType?.length) { - const hasOwnReaction = reactionsOfReactionType.some( - (reaction) => reaction.user_id === clientId, - ); - acc.push({ own: hasOwnReaction, type: reactionType }); - } + const { existingReactions, hasReactions } = useProcessReactions({ + latest_reactions: message.latest_reactions, + own_reactions: message.own_reactions, + reaction_groups: message.reaction_groups, + }); - return acc; - }, [] as Reactions) - : []; + const reactions = hasReactions ? existingReactions : []; const ownCapabilities = useOwnCapabilitiesContext(); diff --git a/package/src/components/Message/MessageSimple/ReactionList.tsx b/package/src/components/Message/MessageSimple/ReactionList.tsx index c798b47410..bd32a89392 100644 --- a/package/src/components/Message/MessageSimple/ReactionList.tsx +++ b/package/src/components/Message/MessageSimple/ReactionList.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { StyleSheet, TouchableOpacity, useWindowDimensions, View } from 'react-native'; +import { StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native'; import Svg, { Circle } from 'react-native-svg'; +import { ReactionGroupResponse, ReactionResponse } from 'stream-chat'; + import { MessageContextValue, - Reactions, useMessageContext, } from '../../../contexts/messageContext/MessageContext'; import { @@ -19,26 +20,10 @@ import { Unknown } from '../../../icons/Unknown'; import type { IconProps } from '../../../icons/utils/base'; import type { DefaultStreamChatGenerics } from '../../../types/types'; import type { ReactionData } from '../../../utils/utils'; - -const styles = StyleSheet.create({ - container: { - left: 0, - position: 'absolute', - top: 0, - }, - reactionBubble: { - alignItems: 'center', - flexDirection: 'row', - justifyContent: 'space-evenly', - position: 'absolute', - }, - reactionBubbleBackground: { - position: 'absolute', - }, -}); +import { ReactionSummary } from '../hooks/useProcessReactions'; export type MessageReactions = { - reactions: Reactions; + reactions: ReactionSummary[]; supportedReactions?: ReactionData[]; }; @@ -76,7 +61,13 @@ export type ReactionListPropsWithContext< messageContentWidth: number; supportedReactions: ReactionData[]; fill?: string; + /** An array of the reaction objects to display in the list */ + latest_reactions?: ReactionResponse[]; + /** An array of the own reaction objects to distinguish own reactions visually */ + own_reactions?: ReactionResponse[] | null; radius?: number; // not recommended to change this + /** An object containing summary for each reaction type on a message */ + reaction_groups?: Record | null; reactionSize?: number; stroke?: string; strokeSize?: number; // not recommended to change this @@ -110,6 +101,7 @@ const ReactionListWithContext = < theme: { colors: { accent_blue, + black, grey, grey_gainsboro, grey_whisper, @@ -125,7 +117,6 @@ const ReactionListWithContext = < middleIcon, radius: themeRadius, reactionBubble, - reactionBubbleBackground, reactionSize: themeReactionSize, strokeSize: themeStrokeSize, }, @@ -200,21 +191,6 @@ const ReactionListWithContext = < - @@ -252,24 +228,36 @@ const ReactionListWithContext = < styles.reactionBubble, { backgroundColor: alignmentLeft ? fill : white, - borderRadius: reactionSize - strokeSize * 2, + borderColor: fill, + borderRadius: reactionSize, + borderWidth: strokeSize, height: reactionSize - strokeSize * 2, left: left + strokeSize, top: strokeSize, - width: reactionSize * reactions.length - strokeSize * 2, }, reactionBubble, ]} > - {reactions.map((reaction) => ( - ( + + style={[ + styles.reactionContainer, + { + marginRight: index < reactions.length - 1 ? 5 : 0, + }, + ]} + > + + {reaction.count} + ))} @@ -285,11 +273,13 @@ const areEqual = type === nextMessage.latest_reactions?.[index].type, + ) + : prevReactions === nextReactions; + + if (!reactionsEqual) return false; + return true; }; @@ -323,7 +323,7 @@ const MemoizedReactionList = React.memo( export type ReactionListProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Partial, 'messageContentWidth'>> & +> = Partial> & Pick, 'messageContentWidth'>; /** @@ -364,3 +364,28 @@ export const ReactionList = < /> ); }; + +const styles = StyleSheet.create({ + container: { + left: 0, + position: 'absolute', + top: 0, + }, + reactionBubble: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'space-evenly', + paddingHorizontal: 5, + position: 'absolute', + }, + reactionContainer: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center', + }, + reactionCount: { + fontSize: 12, + fontWeight: 'bold', + marginLeft: 2, + }, +}); diff --git a/package/src/components/Message/MessageSimple/__tests__/MessageContent.test.js b/package/src/components/Message/MessageSimple/__tests__/MessageContent.test.js index 661e762956..ef06ec7c46 100644 --- a/package/src/components/Message/MessageSimple/__tests__/MessageContent.test.js +++ b/package/src/components/Message/MessageSimple/__tests__/MessageContent.test.js @@ -212,7 +212,7 @@ describe('MessageContent', () => { const user = generateUser(); const reaction = generateReaction(); const message = generateMessage({ - latest_reactions: [reaction], + reaction_groups: { [reaction.type]: reaction }, user, }); diff --git a/package/src/components/Message/hooks/useCreateMessageContext.ts b/package/src/components/Message/hooks/useCreateMessageContext.ts index a4e656dc0c..d1692f31b6 100644 --- a/package/src/components/Message/hooks/useCreateMessageContext.ts +++ b/package/src/components/Message/hooks/useCreateMessageContext.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import type { MessageContextValue } from '../../../contexts/messageContext/MessageContext'; import type { DefaultStreamChatGenerics } from '../../../types/types'; -import { isMessageWithStylesReadByAndDateSeparator } from '../../MessageList/hooks/useMessageList'; +import { stringifyMessage } from '../../../utils/utils'; export const useCreateMessageContext = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, @@ -48,14 +48,9 @@ export const useCreateMessageContext = < videos, }: MessageContextValue) => { const groupStylesLength = groupStyles.length; - const reactionsValue = reactions.map(({ own, type }) => `${own}${type}`).join(); - const latestReactions = message.latest_reactions ? message.latest_reactions : undefined; - const readBy = isMessageWithStylesReadByAndDateSeparator(message) && message.readBy; - const messageValue = `${ - latestReactions ? latestReactions.map(({ type, user }) => `${type}${user?.id}`).join() : '' - }${message.updated_at}${message.deleted_at}${readBy}${message.status}${message.type}${ - message.text - }${message.reply_count}`; + const reactionsValue = reactions.map(({ count, own, type }) => `${own}${type}${count}`).join(); + const stringifiedMessage = stringifyMessage(message); + const membersValue = JSON.stringify(members); const myMessageThemeString = useMemo(() => JSON.stringify(myMessageTheme), [myMessageTheme]); @@ -115,7 +110,7 @@ export const useCreateMessageContext = < lastGroupMessage, lastReceivedId, membersValue, - messageValue, + stringifiedMessage, myMessageThemeString, reactionsValue, showAvatar, diff --git a/package/src/components/Message/hooks/useProcessReactions.ts b/package/src/components/Message/hooks/useProcessReactions.ts new file mode 100644 index 0000000000..a3185cd7dd --- /dev/null +++ b/package/src/components/Message/hooks/useProcessReactions.ts @@ -0,0 +1,116 @@ +import { ComponentType, useMemo } from 'react'; + +import { ReactionResponse } from 'stream-chat'; + +import { + MessagesContextValue, + useMessagesContext, +} from '../../../contexts/messagesContext/MessagesContext'; +import { DefaultStreamChatGenerics } from '../../../types/types'; +import { ReactionData } from '../../../utils/utils'; +import { ReactionListProps } from '../MessageSimple/ReactionList'; + +export type ReactionSummary = { + own: boolean; + type: string; + count?: number; + firstReactionAt?: Date | null; + Icon?: ComponentType | null; + lastReactionAt?: Date | null; + latestReactedUserNames?: string[]; + unlistedReactedUserCount?: number; +}; + +export type ReactionsComparator = (a: ReactionSummary, b: ReactionSummary) => number; + +type UseProcessReactionsParams< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = Pick< + ReactionListProps, + 'own_reactions' | 'reaction_groups' | 'latest_reactions' +> & + Partial, 'supportedReactions'>> & { + sortReactions?: ReactionsComparator; + }; + +export const defaultReactionsSort: ReactionsComparator = (a, b) => { + if (a.firstReactionAt && b.firstReactionAt) { + return +a.firstReactionAt - +b.firstReactionAt; + } + + return a.type.localeCompare(b.type, 'en'); +}; + +const isOwnReaction = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( + reactionType: string, + ownReactions?: ReactionResponse[] | null, +) => (ownReactions ? ownReactions.some((reaction) => reaction.type === reactionType) : false); + +const isSupportedReaction = (reactionType: string, supportedReactions: ReactionData[]) => + supportedReactions + ? supportedReactions.some((reactionOption) => reactionOption.type === reactionType) + : false; + +const getEmojiByReactionType = (reactionType: string, supportedReactions: ReactionData[]) => + supportedReactions.find(({ type }) => type === reactionType)?.Icon ?? null; + +const getLatestReactedUserNames = (reactionType: string, latestReactions?: ReactionResponse[]) => + latestReactions + ? latestReactions.flatMap((reaction) => { + if (reactionType && reactionType === reaction.type) { + const username = reaction.user?.name || reaction.user?.id; + return username ? [username] : []; + } + return []; + }) + : []; + +/** + * Custom hook to process reactions data from message and return a list of reactions with additional info. + */ +export const useProcessReactions = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( + props: UseProcessReactionsParams, +) => { + const { supportedReactions: contextSupportedReactions } = useMessagesContext(); + + const { + latest_reactions, + own_reactions, + reaction_groups, + sortReactions = defaultReactionsSort, + supportedReactions = contextSupportedReactions, + } = props; + + return useMemo(() => { + if (!reaction_groups) + return { existingReactions: [], hasReactions: false, totalReactionCount: 0 }; + const unsortedReactions = Object.entries(reaction_groups).flatMap( + ([reactionType, { count, first_reaction_at, last_reaction_at }]) => { + if (count === 0 || !isSupportedReaction(reactionType, supportedReactions)) return []; + + const latestReactedUserNames = getLatestReactedUserNames(reactionType, latest_reactions); + + return { + count, + firstReactionAt: first_reaction_at ? new Date(first_reaction_at) : null, + Icon: getEmojiByReactionType(reactionType, supportedReactions), + lastReactionAt: last_reaction_at ? new Date(last_reaction_at) : null, + latestReactedUserNames, + own: isOwnReaction(reactionType, own_reactions), + type: reactionType, + unlistedReactedUserCount: count - latestReactedUserNames.length, + }; + }, + ); + + return { + existingReactions: unsortedReactions.sort(sortReactions), + hasReactions: unsortedReactions.length > 0, + totalReactionCount: unsortedReactions.reduce((total, { count }) => total + count, 0), + }; + }, [reaction_groups, own_reactions?.length, latest_reactions?.length, sortReactions]); +}; diff --git a/package/src/contexts/messageContext/MessageContext.tsx b/package/src/contexts/messageContext/MessageContext.tsx index 31b7d7a1b4..6ba0bc49ac 100644 --- a/package/src/contexts/messageContext/MessageContext.tsx +++ b/package/src/contexts/messageContext/MessageContext.tsx @@ -3,6 +3,7 @@ import React, { PropsWithChildren, useContext } from 'react'; import type { Attachment } from 'stream-chat'; import type { ActionHandler } from '../../components/Attachment/Attachment'; +import { ReactionSummary } from '../../components/Message/hooks/useProcessReactions'; import type { MessageTouchableHandlerPayload, TouchableHandlerPayload, @@ -19,11 +20,6 @@ import { getDisplayName } from '../utils/getDisplayName'; export type Alignment = 'right' | 'left'; -export type Reactions = { - own: boolean; - type: string; -}[]; - export type MessageContextValue< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = { @@ -92,7 +88,7 @@ export type MessageContextValue< onPressIn: ((payload: TouchableHandlerPayload) => void) | null; /** The images attached to a message */ otherAttachments: Attachment[]; - reactions: Reactions; + reactions: ReactionSummary[]; /** React set state function to set the state of `isEditedMessageOpen` */ setIsEditedMessageOpen: React.Dispatch>; showMessageOverlay: (messageReactions?: boolean, error?: boolean) => void; diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts index 891b7710b5..40a65f6fef 100644 --- a/package/src/contexts/themeContext/utils/theme.ts +++ b/package/src/contexts/themeContext/utils/theme.ts @@ -533,7 +533,6 @@ export type Theme = { middleIcon: ViewStyle; radius: number; reactionBubble: ViewStyle; - reactionBubbleBackground: ViewStyle; reactionSize: number; strokeSize: number; }; @@ -1120,7 +1119,6 @@ export const defaultTheme: Theme = { middleIcon: {}, radius: 2, // not recommended to change this reactionBubble: {}, - reactionBubbleBackground: {}, reactionSize: 24, strokeSize: 1, // not recommended to change this }, diff --git a/package/src/store/QuickSqliteClient.ts b/package/src/store/QuickSqliteClient.ts index 7eae812dc3..440c96d647 100644 --- a/package/src/store/QuickSqliteClient.ts +++ b/package/src/store/QuickSqliteClient.ts @@ -30,7 +30,7 @@ import type { PreparedQueries, Table } from './types'; * */ export class QuickSqliteClient { - static dbVersion = 3; + static dbVersion = 4; static dbName = DB_NAME; static dbLocation = DB_LOCATION; diff --git a/package/src/store/apis/insertReaction.ts b/package/src/store/apis/insertReaction.ts index e6e3bb2ea3..f9bee7778f 100644 --- a/package/src/store/apis/insertReaction.ts +++ b/package/src/store/apis/insertReaction.ts @@ -1,14 +1,17 @@ -import type { ReactionResponse } from 'stream-chat'; +import type { FormatMessageResponse, MessageResponse, ReactionResponse } from 'stream-chat'; import { mapReactionToStorable } from '../mappers/mapReactionToStorable'; import { QuickSqliteClient } from '../QuickSqliteClient'; +import { createUpdateQuery } from '../sqlite-utils/createUpdateQuery'; import { createUpsertQuery } from '../sqlite-utils/createUpsertQuery'; import type { PreparedQueries } from '../types'; export const insertReaction = ({ flush = true, + message, reaction, }: { + message: MessageResponse | FormatMessageResponse; reaction: ReactionResponse; flush?: boolean; }) => { @@ -18,10 +21,17 @@ export const insertReaction = ({ queries.push(createUpsertQuery('reactions', storableReaction)); - queries.push([ - 'UPDATE messages SET reactionCounts = reactionCounts + 1 WHERE id = ?', - [reaction.message_id], - ]); + const stringifiedNewReactionGroups = JSON.stringify(message.reaction_groups); + + queries.push( + createUpdateQuery( + 'messages', + { + reactionGroups: stringifiedNewReactionGroups, + }, + { id: reaction.message_id }, + ), + ); QuickSqliteClient.logger?.('info', 'insertReaction', { flush, diff --git a/package/src/store/apis/updateReaction.ts b/package/src/store/apis/updateReaction.ts index 3144c4de9f..d7a797cce7 100644 --- a/package/src/store/apis/updateReaction.ts +++ b/package/src/store/apis/updateReaction.ts @@ -36,21 +36,11 @@ export const updateReaction = ({ let updatedReactionCounts: string | undefined; - if (message.reaction_counts) { - const { reactionCounts } = mapMessageToStorable(message); - updatedReactionCounts = reactionCounts; - - queries.push( - createUpdateQuery( - 'messages', - { - reactionCounts, - }, - { - id: message.id, - }, - ), - ); + let updatedReactionGroups: string | undefined; + if (message.reaction_groups) { + const { reactionGroups } = mapMessageToStorable(message); + updatedReactionGroups = reactionGroups; + queries.push(createUpdateQuery('messages', { reactionGroups }, { id: message.id })); } QuickSqliteClient.logger?.('info', 'updateReaction', { @@ -58,6 +48,7 @@ export const updateReaction = ({ flush, updatedReaction: storableReaction, updatedReactionCounts, + updatedReactionGroups, }); if (flush) { diff --git a/package/src/store/mappers/mapMessageToStorable.ts b/package/src/store/mappers/mapMessageToStorable.ts index a366949ed0..b38edf0b97 100644 --- a/package/src/store/mappers/mapMessageToStorable.ts +++ b/package/src/store/mappers/mapMessageToStorable.ts @@ -17,7 +17,7 @@ export const mapMessageToStorable = ( latest_reactions, // eslint-disable-next-line @typescript-eslint/no-unused-vars own_reactions, - reaction_counts, + reaction_groups, text, type, updated_at, @@ -32,7 +32,7 @@ export const mapMessageToStorable = ( deletedAt: mapDateTimeToStorable(deleted_at), extraData: JSON.stringify(extraData), id, - reactionCounts: JSON.stringify(reaction_counts), + reactionGroups: JSON.stringify(reaction_groups), text, type, updatedAt: mapDateTimeToStorable(updated_at), diff --git a/package/src/store/mappers/mapStorableToMessage.ts b/package/src/store/mappers/mapStorableToMessage.ts index 23677c6a02..a051cab001 100644 --- a/package/src/store/mappers/mapStorableToMessage.ts +++ b/package/src/store/mappers/mapStorableToMessage.ts @@ -19,7 +19,7 @@ export const mapStorableToMessage = < messageRow: TableRowJoinedUser<'messages'>; reactionRows: TableRowJoinedUser<'reactions'>[]; }): MessageResponse => { - const { createdAt, deletedAt, extraData, reactionCounts, updatedAt, user, ...rest } = messageRow; + const { createdAt, deletedAt, extraData, reactionGroups, updatedAt, user, ...rest } = messageRow; const latestReactions = reactionRows?.map((reaction) => mapStorableToReaction(reaction)) || []; @@ -32,7 +32,7 @@ export const mapStorableToMessage = < deleted_at: deletedAt, latest_reactions: latestReactions, own_reactions: ownReactions, - reaction_counts: reactionCounts ? JSON.parse(reactionCounts) : {}, + reaction_groups: reactionGroups ? JSON.parse(reactionGroups) : {}, updated_at: updatedAt, user: mapStorableToUser(user), ...(extraData ? JSON.parse(extraData) : {}), diff --git a/package/src/store/schema.ts b/package/src/store/schema.ts index c4ee7ef7cf..4b3834d34e 100644 --- a/package/src/store/schema.ts +++ b/package/src/store/schema.ts @@ -102,7 +102,7 @@ export const tables: Tables = { deletedAt: 'TEXT', extraData: 'TEXT', id: 'TEXT', - reactionCounts: 'TEXT', + reactionGroups: 'TEXT', text: "TEXT DEFAULT ''", type: 'TEXT', updatedAt: 'TEXT', @@ -262,7 +262,7 @@ export type Schema = { deletedAt: string; extraData: string; id: string; - reactionCounts: string; + reactionGroups: string; type: MessageLabel; updatedAt: string; text?: string; diff --git a/package/src/utils/addReactionToLocalState.ts b/package/src/utils/addReactionToLocalState.ts index a9411b8938..b3a81dd613 100644 --- a/package/src/utils/addReactionToLocalState.ts +++ b/package/src/utils/addReactionToLocalState.ts @@ -49,30 +49,72 @@ export const addReactionToLocalState = < message.latest_reactions = []; } message.latest_reactions = message.latest_reactions.filter((r) => r.user_id !== user.id); + if ( currentReaction && - message.reaction_counts && - message.reaction_counts[currentReaction.type] && - message.reaction_counts[currentReaction.type] > 0 + message.reaction_groups && + message.reaction_groups[currentReaction.type] && + message.reaction_groups[currentReaction.type].count > 0 && + message.reaction_groups[currentReaction.type].sum_scores > 0 ) { - message.reaction_counts[currentReaction.type] = - message.reaction_counts[currentReaction.type] - 1; + message.reaction_groups[currentReaction.type].count = + message.reaction_groups[currentReaction.type].count - 1; + message.reaction_groups[currentReaction.type].sum_scores = + message.reaction_groups[currentReaction.type].sum_scores - 1; } - if (!message.reaction_counts) { - message.reaction_counts = { - [reactionType]: 1, + if (!message.reaction_groups) { + message.reaction_groups = { + [reactionType]: { + count: 1, + first_reaction_at: new Date().toISOString(), + last_reaction_at: new Date().toISOString(), + sum_scores: 1, + }, }; } else { - message.reaction_counts[reactionType] = (message.reaction_counts?.[reactionType] || 0) + 1; + if (!message.reaction_groups[reactionType]) { + message.reaction_groups[reactionType] = { + count: 1, + first_reaction_at: new Date().toISOString(), + last_reaction_at: new Date().toISOString(), + sum_scores: 1, + }; + } else { + message.reaction_groups[reactionType] = { + ...message.reaction_groups[reactionType], + count: message.reaction_groups[reactionType].count + 1, + last_reaction_at: new Date().toISOString(), + sum_scores: message.reaction_groups[reactionType].sum_scores + 1, + }; + } } } else { - if (!message.reaction_counts) { - message.reaction_counts = { - [reactionType]: 1, + if (!message.reaction_groups) { + message.reaction_groups = { + [reactionType]: { + count: 1, + first_reaction_at: new Date().toISOString(), + last_reaction_at: new Date().toISOString(), + sum_scores: 1, + }, }; } else { - message.reaction_counts[reactionType] = (message.reaction_counts?.[reactionType] || 0) + 1; + if (!message.reaction_groups[reactionType]) { + message.reaction_groups[reactionType] = { + count: 1, + first_reaction_at: new Date().toISOString(), + last_reaction_at: new Date().toISOString(), + sum_scores: 1, + }; + } else { + message.reaction_groups[reactionType] = { + ...message.reaction_groups[reactionType], + count: message.reaction_groups[reactionType].count + 1, + last_reaction_at: new Date().toISOString(), + sum_scores: message.reaction_groups[reactionType].sum_scores + 1, + }; + } } } @@ -86,6 +128,7 @@ export const addReactionToLocalState = < }); } else { insertReaction({ + message, reaction, }); } diff --git a/package/src/utils/removeReactionFromLocalState.ts b/package/src/utils/removeReactionFromLocalState.ts index 7108b06b92..2cf4534013 100644 --- a/package/src/utils/removeReactionFromLocalState.ts +++ b/package/src/utils/removeReactionFromLocalState.ts @@ -27,8 +27,28 @@ export const removeReactionFromLocalState = < (r) => !(r.user_id === user?.id && r.type === reactionType), ); - if (message.reaction_counts?.[reactionType] && message.reaction_counts?.[reactionType] > 0) { - message.reaction_counts[reactionType] = message.reaction_counts[reactionType] - 1; + if (message.reaction_groups?.[reactionType]) { + if ( + message.reaction_groups[reactionType].count > 0 || + message.reaction_groups[reactionType].sum_scores > 0 + ) { + message.reaction_groups[reactionType].count = Math.max( + 0, + message.reaction_groups[reactionType].count - 1, + ); + message.reaction_groups[reactionType].sum_scores = Math.max( + 0, + message.reaction_groups[reactionType].sum_scores - 1, + ); + if ( + message.reaction_groups[reactionType].count === 0 || + message.reaction_groups[reactionType].sum_scores === 0 + ) { + delete message.reaction_groups[reactionType]; + } + } else { + delete message.reaction_groups[reactionType]; + } } deleteReaction({ diff --git a/package/src/utils/removeReservedFields.ts b/package/src/utils/removeReservedFields.ts index 99e0ed37ef..34642ebd95 100644 --- a/package/src/utils/removeReservedFields.ts +++ b/package/src/utils/removeReservedFields.ts @@ -19,6 +19,7 @@ export const removeReservedFields = < 'latest_reactions', 'own_reactions', 'reaction_counts', + 'reaction_groups', 'last_message_at', 'member_count', 'type', diff --git a/package/src/utils/utils.ts b/package/src/utils/utils.ts index e7e04fb03f..d6d4dcd9d7 100644 --- a/package/src/utils/utils.ts +++ b/package/src/utils/utils.ts @@ -13,7 +13,8 @@ import type { UserResponse, } from 'stream-chat'; -import type { MessageType } from '../components/MessageList/hooks/useMessageList'; +import { IconProps } from '../../src/icons/utils/base'; +import { MessageType } from '../components/MessageList/hooks/useMessageList'; import type { EmojiSearchIndex, MentionAllAppUsersQuery, @@ -24,7 +25,6 @@ import type { SuggestionUser, } from '../contexts/suggestionsContext/SuggestionsContext'; import { compiledEmojis, Emoji } from '../emoji-data'; -import type { IconProps } from '../icons/utils/base'; import type { TableRowJoinedUser } from '../store/types'; import type { DefaultStreamChatGenerics, ValueOf } from '../types/types'; @@ -602,19 +602,31 @@ export const hasOnlyEmojis = (text: string) => { * @param {FormatMessageResponse} message - the message object to be stringified * @returns {string} The stringified message */ -const stringifyMessage = < +export const stringifyMessage = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ deleted_at, latest_reactions, + reaction_groups, + readBy, reply_count, status, + text, type, updated_at, -}: FormatMessageResponse): string => - `${type}${deleted_at}${ - latest_reactions ? latest_reactions.map(({ type }) => type).join() : '' - }${reply_count}${status}${updated_at?.toISOString?.() || updated_at}`; +}: FormatMessageResponse | MessageType): string => + `${ + latest_reactions ? latest_reactions.map(({ type, user }) => `${type}${user?.id}`).join() : '' + }${ + reaction_groups + ? Object.entries(reaction_groups) + .flatMap( + ([type, { count, first_reaction_at, last_reaction_at }]) => + `${type}${count}${first_reaction_at}${last_reaction_at}`, + ) + .join() + : '' + }${type}${deleted_at}${text}${readBy}${reply_count}${status}${updated_at}`; /** * Reduces a list of messages to strings that are used in useEffect & useMemo