From 440b4090f9379336c633b9a0426ac967d49044c8 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Thu, 23 May 2024 15:30:40 +0530 Subject: [PATCH 1/7] fix: reaction list reactions sorting order based on created_at --- package/src/components/Message/Message.tsx | 39 +++++++++++----------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index 3f08e89cad..a700e9e233 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -1,6 +1,7 @@ import React, { useMemo, useState } from 'react'; import { GestureResponderEvent, Keyboard, StyleProp, View, ViewStyle } from 'react-native'; +import { uniqBy } from 'lodash'; import type { Attachment, UserResponse } from 'stream-chat'; import { useCreateMessageContext } from './hooks/useCreateMessageContext'; @@ -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, @@ -473,23 +470,25 @@ const MessageWithContext = < 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, - ); + const getReactions = (reactions: typeof message.latest_reactions) => { + // Firstly, sort the reactions in descending order of created_at + const sortedReactions = reactions?.sort( + (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), + ); - if (reactionsOfReactionType?.length) { - const hasOwnReaction = reactionsOfReactionType.some( - (reaction) => reaction.user_id === clientId, - ); - acc.push({ own: hasOwnReaction, type: reactionType }); - } + // Find unique reactions by type + const uniqueReactions = uniqBy(sortedReactions, 'type'); + + // Assign `own` as true for reactions by the client + const mappedReactions = uniqueReactions.map((reaction) => ({ + own: reaction.user_id === clientId, + type: reaction.type, + })); + + return mappedReactions; + }; - return acc; - }, [] as Reactions) - : []; + const reactions = hasReactions ? getReactions(message.latest_reactions) : []; const ownCapabilities = useOwnCapabilitiesContext(); From 7414c3ffe9ddda57e578d0433dedfd3713860e51 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Mon, 27 May 2024 17:58:39 +0530 Subject: [PATCH 2/7] fix: show reactions using reactions_group --- package/src/components/Channel/Channel.tsx | 3 + package/src/components/Message/Message.tsx | 32 ++--- .../Message/MessageSimple/ReactionList.tsx | 119 +++++++++------- .../Message/hooks/useCreateMessageContext.ts | 17 +-- .../Message/hooks/useProcessReactions.ts | 132 ++++++++++++++++++ .../messageContext/MessageContext.tsx | 8 +- .../src/contexts/themeContext/utils/theme.ts | 2 - package/src/store/apis/insertReaction.ts | 22 ++- package/src/store/apis/updateReaction.ts | 8 ++ .../src/store/mappers/mapMessageToStorable.ts | 2 + .../src/store/mappers/mapStorableToMessage.ts | 12 +- package/src/store/schema.ts | 2 + package/src/utils/addReactionToLocalState.ts | 68 +++++++++ .../src/utils/removeReactionFromLocalState.ts | 30 +++- package/src/utils/removeReservedFields.ts | 1 + package/src/utils/utils.ts | 22 ++- 16 files changed, 377 insertions(+), 103 deletions(-) create mode 100644 package/src/components/Message/hooks/useProcessReactions.ts diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index bfb0f1b545..6cef19b6c8 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -1653,6 +1653,8 @@ const ChannelWithContext = < // eslint-disable-next-line @typescript-eslint/no-unused-vars reaction_counts, // eslint-disable-next-line @typescript-eslint/no-unused-vars + reaction_groups, + // eslint-disable-next-line @typescript-eslint/no-unused-vars reactions, // eslint-disable-next-line @typescript-eslint/no-unused-vars status, @@ -2000,6 +2002,7 @@ const ChannelWithContext = < }, }); }; + const deleteMessage: MessagesContextValue['deleteMessage'] = async ( message, ) => { diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index a700e9e233..321ddaf005 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -1,12 +1,12 @@ import React, { useMemo, useState } from 'react'; import { GestureResponderEvent, Keyboard, StyleProp, View, ViewStyle } from 'react-native'; -import { uniqBy } from 'lodash'; 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 { @@ -465,30 +465,14 @@ const MessageWithContext = < } }; - const hasReactions = - !isMessageTypeDeleted && !!message.latest_reactions && message.latest_reactions.length > 0; - - const clientId = client.userID; - - const getReactions = (reactions: typeof message.latest_reactions) => { - // Firstly, sort the reactions in descending order of created_at - const sortedReactions = reactions?.sort( - (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), - ); - - // Find unique reactions by type - const uniqueReactions = uniqBy(sortedReactions, 'type'); - - // Assign `own` as true for reactions by the client - const mappedReactions = uniqueReactions.map((reaction) => ({ - own: reaction.user_id === clientId, - type: reaction.type, - })); - - return mappedReactions; - }; + const { existingReactions, hasReactions } = useProcessReactions({ + latest_reactions: message.latest_reactions, + own_reactions: message.own_reactions ?? [], + reaction_groups: message.reaction_groups ?? {}, + supportedReactions, + }); - const reactions = hasReactions ? getReactions(message.latest_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..a30b239412 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[]; radius?: number; // not recommended to change this + /** An object containing summary for each reaction type on a message */ + reaction_groups?: Record; 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/hooks/useCreateMessageContext.ts b/package/src/components/Message/hooks/useCreateMessageContext.ts index a4e656dc0c..bb31bb2046 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,11 @@ 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, isOwnReaction, type }) => `${isOwnReaction}${type}${count}`) + .join(); + const stringifiedMessage = stringifyMessage(message); + const membersValue = JSON.stringify(members); const myMessageThemeString = useMemo(() => JSON.stringify(myMessageTheme), [myMessageTheme]); @@ -115,7 +112,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..39ec3f4ca3 --- /dev/null +++ b/package/src/components/Message/hooks/useProcessReactions.ts @@ -0,0 +1,132 @@ +import { ComponentType, useCallback, useMemo } from 'react'; + +import { useMessageContext } from '../../../contexts/messageContext/MessageContext'; +import { + MessagesContextValue, + useMessagesContext, +} from '../../../contexts/messagesContext/MessagesContext'; +import { DefaultStreamChatGenerics } from '../../../types/types'; +import { ReactionListProps } from '../MessageSimple/ReactionList'; + +export type ReactionSummary = { + count: number; + firstReactionAt: Date | null; + Icon: ComponentType | null; + isOwnReaction: boolean; + lastReactionAt: Date | null; + latestReactedUserNames: string[]; + type: 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' +> & + Pick, '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'); +}; + +export const useProcessReactions = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( + props: UseProcessReactionsParams, +) => { + const { + latest_reactions: propLatestReactions, + own_reactions: propOwnReactions, + reaction_groups: propReactionGroups, + sortReactions: propSortReactions, + supportedReactions: propSupportedReactions, + } = props; + + const { message } = useMessageContext(); + const { supportedReactions: contextSupportedReactions } = useMessagesContext(); + const supportedReactions = propSupportedReactions || contextSupportedReactions; + const latestReactions = propLatestReactions || message.latest_reactions; + const ownReactions = propOwnReactions || message?.own_reactions; + const reactionGroups = propReactionGroups || message?.reaction_groups; + const sortReactions = propSortReactions || defaultReactionsSort; + + const isOwnReaction = useCallback( + (reactionType: string) => + ownReactions?.some((reaction) => reaction.type === reactionType) ?? false, + [ownReactions], + ); + + const getEmojiByReactionType = useCallback( + (reactionType: string) => + supportedReactions.find(({ type }) => type === reactionType)?.Icon ?? null, + [supportedReactions], + ); + + const isSupportedReaction = useCallback( + (reactionType: string) => + supportedReactions.some((reactionOption) => reactionOption.type === reactionType), + [supportedReactions], + ); + + const getLatestReactedUserNames = useCallback( + (reactionType?: string) => + latestReactions?.flatMap((reaction) => { + if (reactionType && reactionType === reaction.type) { + const username = reaction.user?.name || reaction.user?.id; + return username ? [username] : []; + } + return []; + }) ?? [], + [latestReactions], + ); + + const existingReactions = useMemo(() => { + if (!reactionGroups) return []; + const unsortedReactions = Object.entries(reactionGroups).flatMap( + ([reactionType, { count, first_reaction_at, last_reaction_at }]) => { + if (count === 0 || !isSupportedReaction(reactionType)) return []; + + const latestReactedUserNames = getLatestReactedUserNames(reactionType); + + return { + count, + firstReactionAt: first_reaction_at ? new Date(first_reaction_at) : null, + Icon: getEmojiByReactionType(reactionType), + isOwnReaction: isOwnReaction(reactionType), + lastReactionAt: last_reaction_at ? new Date(last_reaction_at) : null, + latestReactedUserNames, + type: reactionType, + unlistedReactedUserCount: count - latestReactedUserNames.length, + }; + }, + ); + + return unsortedReactions.sort(sortReactions); + }, [ + getEmojiByReactionType, + getLatestReactedUserNames, + isOwnReaction, + isSupportedReaction, + reactionGroups, + sortReactions, + ]); + + const hasReactions = existingReactions.length > 0; + + const totalReactionCount = useMemo( + () => existingReactions.reduce((total, { count }) => total + count, 0), + [existingReactions], + ); + + return { existingReactions, hasReactions, totalReactionCount }; +}; 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/apis/insertReaction.ts b/package/src/store/apis/insertReaction.ts index e6e3bb2ea3..3bf099ada8 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,19 @@ export const insertReaction = ({ queries.push(createUpsertQuery('reactions', storableReaction)); - queries.push([ - 'UPDATE messages SET reactionCounts = reactionCounts + 1 WHERE id = ?', - [reaction.message_id], - ]); + const stringifiedNewReactionCounts = JSON.stringify(message.reaction_counts); + const stringifiedNewReactionGroups = JSON.stringify(message.reaction_groups); + + queries.push( + createUpdateQuery( + 'messages', + { + reactionCounts: stringifiedNewReactionCounts, + 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..e374a0488f 100644 --- a/package/src/store/apis/updateReaction.ts +++ b/package/src/store/apis/updateReaction.ts @@ -53,11 +53,19 @@ export const updateReaction = ({ ); } + 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', { addedUser: storableUser, 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..6ee96f3aa3 100644 --- a/package/src/store/mappers/mapMessageToStorable.ts +++ b/package/src/store/mappers/mapMessageToStorable.ts @@ -18,6 +18,7 @@ export const mapMessageToStorable = ( // eslint-disable-next-line @typescript-eslint/no-unused-vars own_reactions, reaction_counts, + reaction_groups, text, type, updated_at, @@ -33,6 +34,7 @@ export const mapMessageToStorable = ( 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..846483d7ff 100644 --- a/package/src/store/mappers/mapStorableToMessage.ts +++ b/package/src/store/mappers/mapStorableToMessage.ts @@ -19,7 +19,16 @@ export const mapStorableToMessage = < messageRow: TableRowJoinedUser<'messages'>; reactionRows: TableRowJoinedUser<'reactions'>[]; }): MessageResponse => { - const { createdAt, deletedAt, extraData, reactionCounts, updatedAt, user, ...rest } = messageRow; + const { + createdAt, + deletedAt, + extraData, + reactionCounts, + reactionGroups, + updatedAt, + user, + ...rest + } = messageRow; const latestReactions = reactionRows?.map((reaction) => mapStorableToReaction(reaction)) || []; @@ -33,6 +42,7 @@ export const mapStorableToMessage = < 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..d71e548a38 100644 --- a/package/src/store/schema.ts +++ b/package/src/store/schema.ts @@ -103,6 +103,7 @@ export const tables: Tables = { extraData: 'TEXT', id: 'TEXT', reactionCounts: 'TEXT', + reactionGroups: 'TEXT', text: "TEXT DEFAULT ''", type: 'TEXT', updatedAt: 'TEXT', @@ -263,6 +264,7 @@ export type Schema = { 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..ead1a620bd 100644 --- a/package/src/utils/addReactionToLocalState.ts +++ b/package/src/utils/addReactionToLocalState.ts @@ -59,6 +59,19 @@ export const addReactionToLocalState = < message.reaction_counts[currentReaction.type] - 1; } + if ( + currentReaction && + 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_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, @@ -66,6 +79,33 @@ export const addReactionToLocalState = < } else { message.reaction_counts[reactionType] = (message.reaction_counts?.[reactionType] || 0) + 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 { + 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 = { @@ -74,6 +114,33 @@ export const addReactionToLocalState = < } else { message.reaction_counts[reactionType] = (message.reaction_counts?.[reactionType] || 0) + 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 { + 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, + }; + } + } } message.own_reactions = [...message.own_reactions, reaction]; @@ -86,6 +153,7 @@ export const addReactionToLocalState = < }); } else { insertReaction({ + message, reaction, }); } diff --git a/package/src/utils/removeReactionFromLocalState.ts b/package/src/utils/removeReactionFromLocalState.ts index 7108b06b92..cf8ecd6289 100644 --- a/package/src/utils/removeReactionFromLocalState.ts +++ b/package/src/utils/removeReactionFromLocalState.ts @@ -27,8 +27,34 @@ 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 = message.reaction_groups[reactionType].count - 1; + message.reaction_groups[reactionType].sum_scores = + 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]; + } + } + + if (message.reaction_counts?.[reactionType]) { + if (message.reaction_counts[reactionType] > 0) { + message.reaction_counts[reactionType] = message.reaction_counts[reactionType] - 1; + if (message.reaction_counts[reactionType] === 0) { + delete message.reaction_counts[reactionType]; + } + } else { + delete message.reaction_counts[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..5d4021cc9d 100644 --- a/package/src/utils/utils.ts +++ b/package/src/utils/utils.ts @@ -13,7 +13,7 @@ import type { UserResponse, } from 'stream-chat'; -import type { MessageType } from '../components/MessageList/hooks/useMessageList'; +import { MessageType } from '../components/MessageList/hooks/useMessageList'; import type { EmojiSearchIndex, MentionAllAppUsersQuery, @@ -602,19 +602,29 @@ 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 => + `${type}${deleted_at}${latest_reactions ? latest_reactions.map(({ type }) => type).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() + : '' + }${text}${readBy}${reply_count}${status}${updated_at}`; /** * Reduces a list of messages to strings that are used in useEffect & useMemo From 3e8eb052745ff52e350a822bc8941952f4e3169f Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Mon, 27 May 2024 18:09:02 +0530 Subject: [PATCH 3/7] fix: tests --- package/src/components/Message/Message.tsx | 2 +- .../Message/MessageSimple/__tests__/MessageContent.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index 321ddaf005..2221f9ff6a 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -466,7 +466,7 @@ const MessageWithContext = < }; const { existingReactions, hasReactions } = useProcessReactions({ - latest_reactions: message.latest_reactions, + latest_reactions: message.latest_reactions ?? [], own_reactions: message.own_reactions ?? [], reaction_groups: message.reaction_groups ?? {}, supportedReactions, 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, }); From 1547041049ecceace67f88f952bcc3b559f4ed40 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Mon, 27 May 2024 21:53:10 +0530 Subject: [PATCH 4/7] fix: useProcessReactions hook --- package/src/components/Channel/Channel.tsx | 37 +------- .../Message/hooks/useProcessReactions.ts | 91 +++++++++---------- package/src/utils/utils.ts | 3 +- 3 files changed, 46 insertions(+), 85 deletions(-) diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 6cef19b6c8..9c729e18d9 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -1631,42 +1631,7 @@ 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 - reaction_groups, - // 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 { attachments, id, mentioned_users, parent_id, text, ...extraFields } = updatedMessage; if (!channel.id) return; const mentionedUserIds = mentioned_users?.map((user) => user.id) || []; diff --git a/package/src/components/Message/hooks/useProcessReactions.ts b/package/src/components/Message/hooks/useProcessReactions.ts index 39ec3f4ca3..707610fb8f 100644 --- a/package/src/components/Message/hooks/useProcessReactions.ts +++ b/package/src/components/Message/hooks/useProcessReactions.ts @@ -1,4 +1,6 @@ -import { ComponentType, useCallback, useMemo } from 'react'; +import { ComponentType, useMemo } from 'react'; + +import { ReactionResponse } from 'stream-chat'; import { useMessageContext } from '../../../contexts/messageContext/MessageContext'; import { @@ -6,6 +8,7 @@ import { useMessagesContext, } from '../../../contexts/messagesContext/MessagesContext'; import { DefaultStreamChatGenerics } from '../../../types/types'; +import { ReactionData } from '../../../utils/utils'; import { ReactionListProps } from '../MessageSimple/ReactionList'; export type ReactionSummary = { @@ -39,6 +42,32 @@ export const defaultReactionsSort: ReactionsComparator = (a, b) => { 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 []; + }) + : []; + export const useProcessReactions = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( @@ -56,53 +85,24 @@ export const useProcessReactions = < const { supportedReactions: contextSupportedReactions } = useMessagesContext(); const supportedReactions = propSupportedReactions || contextSupportedReactions; const latestReactions = propLatestReactions || message.latest_reactions; - const ownReactions = propOwnReactions || message?.own_reactions; - const reactionGroups = propReactionGroups || message?.reaction_groups; + const ownReactions = propOwnReactions || message.own_reactions; + const reactionGroups = propReactionGroups || message.reaction_groups; const sortReactions = propSortReactions || defaultReactionsSort; - const isOwnReaction = useCallback( - (reactionType: string) => - ownReactions?.some((reaction) => reaction.type === reactionType) ?? false, - [ownReactions], - ); - - const getEmojiByReactionType = useCallback( - (reactionType: string) => - supportedReactions.find(({ type }) => type === reactionType)?.Icon ?? null, - [supportedReactions], - ); - - const isSupportedReaction = useCallback( - (reactionType: string) => - supportedReactions.some((reactionOption) => reactionOption.type === reactionType), - [supportedReactions], - ); - - const getLatestReactedUserNames = useCallback( - (reactionType?: string) => - latestReactions?.flatMap((reaction) => { - if (reactionType && reactionType === reaction.type) { - const username = reaction.user?.name || reaction.user?.id; - return username ? [username] : []; - } - return []; - }) ?? [], - [latestReactions], - ); - - const existingReactions = useMemo(() => { - if (!reactionGroups) return []; + const { existingReactions, hasReactions, totalReactionCount } = useMemo(() => { + if (!reactionGroups) + return { existingReactions: [], hasReactions: false, totalReactionCount: 0 }; const unsortedReactions = Object.entries(reactionGroups).flatMap( ([reactionType, { count, first_reaction_at, last_reaction_at }]) => { - if (count === 0 || !isSupportedReaction(reactionType)) return []; + if (count === 0 || !isSupportedReaction(reactionType, supportedReactions)) return []; - const latestReactedUserNames = getLatestReactedUserNames(reactionType); + const latestReactedUserNames = getLatestReactedUserNames(reactionType, latestReactions); return { count, firstReactionAt: first_reaction_at ? new Date(first_reaction_at) : null, - Icon: getEmojiByReactionType(reactionType), - isOwnReaction: isOwnReaction(reactionType), + Icon: getEmojiByReactionType(reactionType, supportedReactions), + isOwnReaction: isOwnReaction(reactionType, ownReactions), lastReactionAt: last_reaction_at ? new Date(last_reaction_at) : null, latestReactedUserNames, type: reactionType, @@ -111,7 +111,11 @@ export const useProcessReactions = < }, ); - return unsortedReactions.sort(sortReactions); + return { + existingReactions: unsortedReactions.sort(sortReactions), + hasReactions: unsortedReactions.length > 0, + totalReactionCount: unsortedReactions.reduce((total, { count }) => total + count, 0), + }; }, [ getEmojiByReactionType, getLatestReactedUserNames, @@ -121,12 +125,5 @@ export const useProcessReactions = < sortReactions, ]); - const hasReactions = existingReactions.length > 0; - - const totalReactionCount = useMemo( - () => existingReactions.reduce((total, { count }) => total + count, 0), - [existingReactions], - ); - return { existingReactions, hasReactions, totalReactionCount }; }; diff --git a/package/src/utils/utils.ts b/package/src/utils/utils.ts index 5d4021cc9d..3830b34983 100644 --- a/package/src/utils/utils.ts +++ b/package/src/utils/utils.ts @@ -24,12 +24,11 @@ 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'; export type ReactionData = { - Icon: React.ComponentType; + Icon: React.ComponentType; type: string; }; From e014bb21145c67208217364abced9a0823c7a112 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Tue, 28 May 2024 12:44:03 +0530 Subject: [PATCH 5/7] fix: useProcessReactions hook and Channel message spread --- package/src/components/Channel/Channel.tsx | 24 +++++++++- package/src/components/Message/Message.tsx | 7 ++- .../Message/MessageSimple/ReactionList.tsx | 4 +- .../Message/hooks/useProcessReactions.ts | 44 ++++++------------- package/src/utils/utils.ts | 6 ++- 5 files changed, 46 insertions(+), 39 deletions(-) diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 9c729e18d9..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,7 +1632,28 @@ const ChannelWithContext = < ) => { try { const updatedMessage = await uploadPendingAttachments(message); - const { attachments, id, mentioned_users, parent_id, text, ...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) || []; diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index 2221f9ff6a..866daaf44c 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -466,10 +466,9 @@ const MessageWithContext = < }; const { existingReactions, hasReactions } = useProcessReactions({ - latest_reactions: message.latest_reactions ?? [], - own_reactions: message.own_reactions ?? [], - reaction_groups: message.reaction_groups ?? {}, - supportedReactions, + latest_reactions: message.latest_reactions, + own_reactions: message.own_reactions, + reaction_groups: message.reaction_groups, }); const reactions = hasReactions ? existingReactions : []; diff --git a/package/src/components/Message/MessageSimple/ReactionList.tsx b/package/src/components/Message/MessageSimple/ReactionList.tsx index a30b239412..389d8c334a 100644 --- a/package/src/components/Message/MessageSimple/ReactionList.tsx +++ b/package/src/components/Message/MessageSimple/ReactionList.tsx @@ -64,10 +64,10 @@ export type ReactionListPropsWithContext< /** 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[]; + 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; + reaction_groups?: Record | null; reactionSize?: number; stroke?: string; strokeSize?: number; // not recommended to change this diff --git a/package/src/components/Message/hooks/useProcessReactions.ts b/package/src/components/Message/hooks/useProcessReactions.ts index 707610fb8f..3589edacd1 100644 --- a/package/src/components/Message/hooks/useProcessReactions.ts +++ b/package/src/components/Message/hooks/useProcessReactions.ts @@ -2,7 +2,6 @@ import { ComponentType, useMemo } from 'react'; import { ReactionResponse } from 'stream-chat'; -import { useMessageContext } from '../../../contexts/messageContext/MessageContext'; import { MessagesContextValue, useMessagesContext, @@ -30,7 +29,7 @@ type UseProcessReactionsParams< ReactionListProps, 'own_reactions' | 'reaction_groups' | 'latest_reactions' > & - Pick, 'supportedReactions'> & { + Partial, 'supportedReactions'>> & { sortReactions?: ReactionsComparator; }; @@ -73,36 +72,30 @@ export const useProcessReactions = < >( props: UseProcessReactionsParams, ) => { + const { supportedReactions: contextSupportedReactions } = useMessagesContext(); + const { - latest_reactions: propLatestReactions, - own_reactions: propOwnReactions, - reaction_groups: propReactionGroups, - sortReactions: propSortReactions, - supportedReactions: propSupportedReactions, + latest_reactions, + own_reactions, + reaction_groups, + sortReactions = defaultReactionsSort, + supportedReactions = contextSupportedReactions, } = props; - const { message } = useMessageContext(); - const { supportedReactions: contextSupportedReactions } = useMessagesContext(); - const supportedReactions = propSupportedReactions || contextSupportedReactions; - const latestReactions = propLatestReactions || message.latest_reactions; - const ownReactions = propOwnReactions || message.own_reactions; - const reactionGroups = propReactionGroups || message.reaction_groups; - const sortReactions = propSortReactions || defaultReactionsSort; - - const { existingReactions, hasReactions, totalReactionCount } = useMemo(() => { - if (!reactionGroups) + return useMemo(() => { + if (!reaction_groups) return { existingReactions: [], hasReactions: false, totalReactionCount: 0 }; - const unsortedReactions = Object.entries(reactionGroups).flatMap( + 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, latestReactions); + const latestReactedUserNames = getLatestReactedUserNames(reactionType, latest_reactions); return { count, firstReactionAt: first_reaction_at ? new Date(first_reaction_at) : null, Icon: getEmojiByReactionType(reactionType, supportedReactions), - isOwnReaction: isOwnReaction(reactionType, ownReactions), + isOwnReaction: isOwnReaction(reactionType, own_reactions), lastReactionAt: last_reaction_at ? new Date(last_reaction_at) : null, latestReactedUserNames, type: reactionType, @@ -116,14 +109,5 @@ export const useProcessReactions = < hasReactions: unsortedReactions.length > 0, totalReactionCount: unsortedReactions.reduce((total, { count }) => total + count, 0), }; - }, [ - getEmojiByReactionType, - getLatestReactedUserNames, - isOwnReaction, - isSupportedReaction, - reactionGroups, - sortReactions, - ]); - - return { existingReactions, hasReactions, totalReactionCount }; + }, [reaction_groups, own_reactions?.length, latest_reactions?.length, sortReactions]); }; diff --git a/package/src/utils/utils.ts b/package/src/utils/utils.ts index 3830b34983..d3fcee99d3 100644 --- a/package/src/utils/utils.ts +++ b/package/src/utils/utils.ts @@ -614,7 +614,9 @@ export const stringifyMessage = < type, updated_at, }: FormatMessageResponse | MessageType): string => - `${type}${deleted_at}${latest_reactions ? latest_reactions.map(({ type }) => type).join() : ''}${ + `${ + latest_reactions ? latest_reactions.map(({ type, user }) => `${type}${user?.id}`).join() : '' + }${ reaction_groups ? Object.entries(reaction_groups) .flatMap( @@ -623,7 +625,7 @@ export const stringifyMessage = < ) .join() : '' - }${text}${readBy}${reply_count}${status}${updated_at}`; + }${type}${deleted_at}${text}${readBy}${reply_count}${status}${updated_at}`; /** * Reduces a list of messages to strings that are used in useEffect & useMemo From 54cccbad154f9a4acf057c7892bc8bc52c0fe58e Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Fri, 31 May 2024 18:56:07 +0530 Subject: [PATCH 6/7] fix: remove reaction_counts from offline DB --- .../Chat/hooks/handleEventToSyncDB.ts | 3 +-- package/src/store/QuickSqliteClient.ts | 2 +- package/src/store/apis/insertReaction.ts | 2 -- package/src/store/apis/updateReaction.ts | 17 ------------- .../src/store/mappers/mapMessageToStorable.ts | 2 -- .../src/store/mappers/mapStorableToMessage.ts | 12 +-------- package/src/store/schema.ts | 2 -- package/src/utils/addReactionToLocalState.ts | 25 ------------------- .../src/utils/removeReactionFromLocalState.ts | 11 -------- 9 files changed, 3 insertions(+), 73 deletions(-) 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/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 3bf099ada8..f9bee7778f 100644 --- a/package/src/store/apis/insertReaction.ts +++ b/package/src/store/apis/insertReaction.ts @@ -21,14 +21,12 @@ export const insertReaction = ({ queries.push(createUpsertQuery('reactions', storableReaction)); - const stringifiedNewReactionCounts = JSON.stringify(message.reaction_counts); const stringifiedNewReactionGroups = JSON.stringify(message.reaction_groups); queries.push( createUpdateQuery( 'messages', { - reactionCounts: stringifiedNewReactionCounts, reactionGroups: stringifiedNewReactionGroups, }, { id: reaction.message_id }, diff --git a/package/src/store/apis/updateReaction.ts b/package/src/store/apis/updateReaction.ts index e374a0488f..d7a797cce7 100644 --- a/package/src/store/apis/updateReaction.ts +++ b/package/src/store/apis/updateReaction.ts @@ -36,23 +36,6 @@ 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); diff --git a/package/src/store/mappers/mapMessageToStorable.ts b/package/src/store/mappers/mapMessageToStorable.ts index 6ee96f3aa3..b38edf0b97 100644 --- a/package/src/store/mappers/mapMessageToStorable.ts +++ b/package/src/store/mappers/mapMessageToStorable.ts @@ -17,7 +17,6 @@ export const mapMessageToStorable = ( latest_reactions, // eslint-disable-next-line @typescript-eslint/no-unused-vars own_reactions, - reaction_counts, reaction_groups, text, type, @@ -33,7 +32,6 @@ export const mapMessageToStorable = ( deletedAt: mapDateTimeToStorable(deleted_at), extraData: JSON.stringify(extraData), id, - reactionCounts: JSON.stringify(reaction_counts), reactionGroups: JSON.stringify(reaction_groups), text, type, diff --git a/package/src/store/mappers/mapStorableToMessage.ts b/package/src/store/mappers/mapStorableToMessage.ts index 846483d7ff..a051cab001 100644 --- a/package/src/store/mappers/mapStorableToMessage.ts +++ b/package/src/store/mappers/mapStorableToMessage.ts @@ -19,16 +19,7 @@ export const mapStorableToMessage = < messageRow: TableRowJoinedUser<'messages'>; reactionRows: TableRowJoinedUser<'reactions'>[]; }): MessageResponse => { - const { - createdAt, - deletedAt, - extraData, - reactionCounts, - reactionGroups, - updatedAt, - user, - ...rest - } = messageRow; + const { createdAt, deletedAt, extraData, reactionGroups, updatedAt, user, ...rest } = messageRow; const latestReactions = reactionRows?.map((reaction) => mapStorableToReaction(reaction)) || []; @@ -41,7 +32,6 @@ 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), diff --git a/package/src/store/schema.ts b/package/src/store/schema.ts index d71e548a38..4b3834d34e 100644 --- a/package/src/store/schema.ts +++ b/package/src/store/schema.ts @@ -102,7 +102,6 @@ export const tables: Tables = { deletedAt: 'TEXT', extraData: 'TEXT', id: 'TEXT', - reactionCounts: 'TEXT', reactionGroups: 'TEXT', text: "TEXT DEFAULT ''", type: 'TEXT', @@ -263,7 +262,6 @@ export type Schema = { deletedAt: string; extraData: string; id: string; - reactionCounts: string; reactionGroups: string; type: MessageLabel; updatedAt: string; diff --git a/package/src/utils/addReactionToLocalState.ts b/package/src/utils/addReactionToLocalState.ts index ead1a620bd..b3a81dd613 100644 --- a/package/src/utils/addReactionToLocalState.ts +++ b/package/src/utils/addReactionToLocalState.ts @@ -49,15 +49,6 @@ 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_counts[currentReaction.type] = - message.reaction_counts[currentReaction.type] - 1; - } if ( currentReaction && @@ -72,14 +63,6 @@ export const addReactionToLocalState = < message.reaction_groups[currentReaction.type].sum_scores - 1; } - if (!message.reaction_counts) { - message.reaction_counts = { - [reactionType]: 1, - }; - } else { - message.reaction_counts[reactionType] = (message.reaction_counts?.[reactionType] || 0) + 1; - } - if (!message.reaction_groups) { message.reaction_groups = { [reactionType]: { @@ -107,14 +90,6 @@ export const addReactionToLocalState = < } } } else { - if (!message.reaction_counts) { - message.reaction_counts = { - [reactionType]: 1, - }; - } else { - message.reaction_counts[reactionType] = (message.reaction_counts?.[reactionType] || 0) + 1; - } - if (!message.reaction_groups) { message.reaction_groups = { [reactionType]: { diff --git a/package/src/utils/removeReactionFromLocalState.ts b/package/src/utils/removeReactionFromLocalState.ts index cf8ecd6289..12f7f96a99 100644 --- a/package/src/utils/removeReactionFromLocalState.ts +++ b/package/src/utils/removeReactionFromLocalState.ts @@ -46,17 +46,6 @@ export const removeReactionFromLocalState = < } } - if (message.reaction_counts?.[reactionType]) { - if (message.reaction_counts[reactionType] > 0) { - message.reaction_counts[reactionType] = message.reaction_counts[reactionType] - 1; - if (message.reaction_counts[reactionType] === 0) { - delete message.reaction_counts[reactionType]; - } - } else { - delete message.reaction_counts[reactionType]; - } - } - deleteReaction({ messageId, reactionType, From 6cc707595f66856e73a7ad68acee673307ef1a29 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Thu, 6 Jun 2024 17:26:16 +0530 Subject: [PATCH 7/7] fix: reaction summary type --- .../Message/MessageSimple/ReactionList.tsx | 2 +- .../Message/hooks/useCreateMessageContext.ts | 4 +--- .../Message/hooks/useProcessReactions.ts | 19 +++++++++++-------- .../src/utils/removeReactionFromLocalState.ts | 11 ++++++++--- package/src/utils/utils.ts | 3 ++- 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/package/src/components/Message/MessageSimple/ReactionList.tsx b/package/src/components/Message/MessageSimple/ReactionList.tsx index 389d8c334a..bd32a89392 100644 --- a/package/src/components/Message/MessageSimple/ReactionList.tsx +++ b/package/src/components/Message/MessageSimple/ReactionList.tsx @@ -250,7 +250,7 @@ const ReactionListWithContext = < > ) => { const groupStylesLength = groupStyles.length; - const reactionsValue = reactions - .map(({ count, isOwnReaction, type }) => `${isOwnReaction}${type}${count}`) - .join(); + const reactionsValue = reactions.map(({ count, own, type }) => `${own}${type}${count}`).join(); const stringifiedMessage = stringifyMessage(message); const membersValue = JSON.stringify(members); diff --git a/package/src/components/Message/hooks/useProcessReactions.ts b/package/src/components/Message/hooks/useProcessReactions.ts index 3589edacd1..a3185cd7dd 100644 --- a/package/src/components/Message/hooks/useProcessReactions.ts +++ b/package/src/components/Message/hooks/useProcessReactions.ts @@ -11,14 +11,14 @@ import { ReactionData } from '../../../utils/utils'; import { ReactionListProps } from '../MessageSimple/ReactionList'; export type ReactionSummary = { - count: number; - firstReactionAt: Date | null; - Icon: ComponentType | null; - isOwnReaction: boolean; - lastReactionAt: Date | null; - latestReactedUserNames: string[]; + own: boolean; type: string; - unlistedReactedUserCount: number; + count?: number; + firstReactionAt?: Date | null; + Icon?: ComponentType | null; + lastReactionAt?: Date | null; + latestReactedUserNames?: string[]; + unlistedReactedUserCount?: number; }; export type ReactionsComparator = (a: ReactionSummary, b: ReactionSummary) => number; @@ -67,6 +67,9 @@ const getLatestReactedUserNames = (reactionType: string, latestReactions?: React }) : []; +/** + * Custom hook to process reactions data from message and return a list of reactions with additional info. + */ export const useProcessReactions = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( @@ -95,9 +98,9 @@ export const useProcessReactions = < count, firstReactionAt: first_reaction_at ? new Date(first_reaction_at) : null, Icon: getEmojiByReactionType(reactionType, supportedReactions), - isOwnReaction: isOwnReaction(reactionType, own_reactions), lastReactionAt: last_reaction_at ? new Date(last_reaction_at) : null, latestReactedUserNames, + own: isOwnReaction(reactionType, own_reactions), type: reactionType, unlistedReactedUserCount: count - latestReactedUserNames.length, }; diff --git a/package/src/utils/removeReactionFromLocalState.ts b/package/src/utils/removeReactionFromLocalState.ts index 12f7f96a99..2cf4534013 100644 --- a/package/src/utils/removeReactionFromLocalState.ts +++ b/package/src/utils/removeReactionFromLocalState.ts @@ -32,9 +32,14 @@ export const removeReactionFromLocalState = < message.reaction_groups[reactionType].count > 0 || message.reaction_groups[reactionType].sum_scores > 0 ) { - message.reaction_groups[reactionType].count = message.reaction_groups[reactionType].count - 1; - message.reaction_groups[reactionType].sum_scores = - message.reaction_groups[reactionType].sum_scores - 1; + 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 diff --git a/package/src/utils/utils.ts b/package/src/utils/utils.ts index d3fcee99d3..d6d4dcd9d7 100644 --- a/package/src/utils/utils.ts +++ b/package/src/utils/utils.ts @@ -13,6 +13,7 @@ import type { UserResponse, } from 'stream-chat'; +import { IconProps } from '../../src/icons/utils/base'; import { MessageType } from '../components/MessageList/hooks/useMessageList'; import type { EmojiSearchIndex, @@ -28,7 +29,7 @@ import type { TableRowJoinedUser } from '../store/types'; import type { DefaultStreamChatGenerics, ValueOf } from '../types/types'; export type ReactionData = { - Icon: React.ComponentType; + Icon: React.ComponentType; type: string; };