From 440b4090f9379336c633b9a0426ac967d49044c8 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Thu, 23 May 2024 15:30:40 +0530 Subject: [PATCH 01/14] 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 02/14] 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 03/14] 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 04/14] 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 05/14] 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 92f68e8b0f9e3da3034f818bf0fbfe6fa15d3def Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Wed, 29 May 2024 18:27:14 +0530 Subject: [PATCH 06/14] feat: use queryReactions API to query and show reactions in OverlayReactions --- .../MessageOverlay/MessageOverlay.tsx | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/package/src/components/MessageOverlay/MessageOverlay.tsx b/package/src/components/MessageOverlay/MessageOverlay.tsx index 6ede17f01d..49d9c6d739 100644 --- a/package/src/components/MessageOverlay/MessageOverlay.tsx +++ b/package/src/components/MessageOverlay/MessageOverlay.tsx @@ -44,10 +44,7 @@ import { mergeThemes, ThemeProvider, useTheme } from '../../contexts/themeContex import { useViewport } from '../../hooks/useViewport'; import type { DefaultStreamChatGenerics } from '../../types/types'; import { MessageTextContainer } from '../Message/MessageSimple/MessageTextContainer'; -import { - OverlayReactions as DefaultOverlayReactions, - Reaction, -} from '../MessageOverlay/OverlayReactions'; +import { OverlayReactions as DefaultOverlayReactions } from '../MessageOverlay/OverlayReactions'; import type { ReplyProps } from '../Reply/Reply'; const styles = StyleSheet.create({ @@ -464,26 +461,16 @@ const MessageOverlayWithContext = < message={message} /> )} - {!!messageReactionTitle && - message.latest_reactions && - message.latest_reactions.length > 0 ? ( + {!!messageReactionTitle && ( ({ - alignment: clientId && clientId === reaction.user?.id ? 'right' : 'left', - id: reaction?.user?.id || '', - image: reaction?.user?.image, - name: reaction?.user?.name || reaction.user_id || '', - type: reaction.type, - })) as Reaction[] - } showScreen={showScreen} supportedReactions={messagesContext?.supportedReactions} title={messageReactionTitle} /> - ) : null} + )} )} From 772e2fda9f8eefb09ea856ff695a94692598e075 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Wed, 29 May 2024 18:30:55 +0530 Subject: [PATCH 07/14] feat: use queryReactions API to query and show reactions in OverlayReactions --- .../ui-components/overlay-reactions.mdx | 14 +- .../MessageOverlay/OverlayReactions.tsx | 244 +++++------------- .../MessageOverlay/OverlayReactionsItem.tsx | 188 ++++++++++++++ .../MessageOverlay/hooks/useFetchReactions.ts | 88 +++++++ package/src/store/apis/getReactions.ts | 21 ++ .../store/apis/getReactionsforFilterSort.ts | 43 +++ .../queries/selectReactionsForMessages.ts | 6 +- 7 files changed, 423 insertions(+), 181 deletions(-) create mode 100644 package/src/components/MessageOverlay/OverlayReactionsItem.tsx create mode 100644 package/src/components/MessageOverlay/hooks/useFetchReactions.ts create mode 100644 package/src/store/apis/getReactions.ts create mode 100644 package/src/store/apis/getReactionsforFilterSort.ts diff --git a/docusaurus/docs/reactnative/ui-components/overlay-reactions.mdx b/docusaurus/docs/reactnative/ui-components/overlay-reactions.mdx index a46d23d3d4..f316ab61e6 100644 --- a/docusaurus/docs/reactnative/ui-components/overlay-reactions.mdx +++ b/docusaurus/docs/reactnative/ui-components/overlay-reactions.mdx @@ -17,6 +17,14 @@ This is the default component provided to the prop [`OverlayReactions`](../core- +### `messageId` + +The message ID for which the reactions are displayed. + +| Type | Default | +| ----------------------- | ----------- | +| `String` \| `undefined` | `undefined` | + ### `reactions` List of existing reactions which can be extracted from a message. @@ -31,9 +39,9 @@ const reactions = message.latest_reactions.map(reaction => ({ })); ``` -| Type | -| ----- | -| Array | +| Type | Default | +| ---------------------- | ----------- | +| `Array` \| `undefined` | `undefined` | ###
required
`showScreen` {#showscreen} diff --git a/package/src/components/MessageOverlay/OverlayReactions.tsx b/package/src/components/MessageOverlay/OverlayReactions.tsx index 7c443fa59d..c295dcde84 100644 --- a/package/src/components/MessageOverlay/OverlayReactions.tsx +++ b/package/src/components/MessageOverlay/OverlayReactions.tsx @@ -2,7 +2,10 @@ import React from 'react'; import { StyleSheet, Text, useWindowDimensions, View, ViewStyle } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; import Animated, { interpolate, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; -import Svg, { Circle } from 'react-native-svg'; + +import { useFetchReactions } from './hooks/useFetchReactions'; + +import { OverlayReactionsItem } from './OverlayReactionsItem'; import type { Alignment } from '../../contexts/messageContext/MessageContext'; import type { MessageOverlayContextValue } from '../../contexts/messageOverlayContext/MessageOverlayContext'; @@ -12,7 +15,6 @@ import { LoveReaction, ThumbsDownReaction, ThumbsUpReaction, - Unknown, WutReaction, } from '../../icons'; @@ -23,21 +25,6 @@ const styles = StyleSheet.create({ avatarContainer: { padding: 8, }, - avatarInnerContainer: { - alignSelf: 'center', - }, - avatarName: { - flex: 1, - fontSize: 12, - fontWeight: '700', - paddingTop: 6, - textAlign: 'center', - }, - avatarNameContainer: { - alignItems: 'center', - flexDirection: 'row', - flexGrow: 1, - }, container: { alignItems: 'center', borderRadius: 16, @@ -52,18 +39,6 @@ const styles = StyleSheet.create({ alignItems: 'center', paddingBottom: 12, }, - reactionBubble: { - alignItems: 'center', - borderRadius: 24, - justifyContent: 'center', - position: 'absolute', - }, - reactionBubbleBackground: { - borderRadius: 24, - height: 24, - position: 'absolute', - width: 24, - }, title: { fontSize: 16, fontWeight: '700', @@ -109,58 +84,55 @@ export type Reaction = { export type OverlayReactionsProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Pick, 'OverlayReactionsAvatar'> & { - reactions: Reaction[]; showScreen: Animated.SharedValue; title: string; alignment?: Alignment; + messageId?: string; + reactions?: Reaction[]; supportedReactions?: ReactionData[]; }; -type ReactionIconProps = Pick & { - pathFill: string; - size: number; - supportedReactions: ReactionData[]; -}; - -const ReactionIcon = ({ pathFill, size, supportedReactions, type }: ReactionIconProps) => { - const Icon = supportedReactions.find((reaction) => reaction.type === type)?.Icon || Unknown; - return ; -}; - /** * OverlayReactions - A high level component which implements all the logic required for message overlay reactions */ export const OverlayReactions = (props: OverlayReactionsProps) => { + const [itemHeight, setItemHeight] = React.useState(0); const { alignment: overlayAlignment, + messageId, OverlayReactionsAvatar, - reactions, + reactions: propReactions, showScreen, supportedReactions = reactionData, title, } = props; const layoutHeight = useSharedValue(0); const layoutWidth = useSharedValue(0); - - const [itemHeight, setItemHeight] = React.useState(0); + const { + loading, + loadNextPage, + reactions: fetchedReactions, + } = useFetchReactions({ + messageId, + sort: { created_at: -1 }, + }); + + const reactions = + propReactions || + (fetchedReactions.map((reaction) => ({ + alignment: 'left', + id: reaction.user?.id, + image: reaction.user?.image, + name: reaction.user?.name, + type: reaction.type, + })) as Reaction[]); const { theme: { - colors: { accent_blue, black, grey_gainsboro, white }, + colors: { black, white }, overlay: { padding: overlayPadding, - reactions: { - avatarContainer, - avatarName, - avatarSize, - container, - flatListContainer, - radius, - reactionBubble, - reactionBubbleBackground, - reactionBubbleBorderRadius, - title: titleStyle, - }, + reactions: { avatarContainer, avatarSize, container, flatListContainer, title: titleStyle }, }, }, } = useTheme(); @@ -185,100 +157,13 @@ export const OverlayReactions = (props: OverlayReactionsProps) => { (avatarSize + (Number(avatarContainer.padding || 0) || styles.avatarContainer.padding) * 2), ); - const renderItem = ({ item }: { item: Reaction }) => { - const { alignment = 'left', name, type } = item; - const x = avatarSize / 2 - (avatarSize / (radius * 4)) * (alignment === 'left' ? 1 : -1); - const y = avatarSize - radius; - - const left = - alignment === 'left' - ? x - - (Number(reactionBubbleBackground.width || 0) || styles.reactionBubbleBackground.width) + - radius - : x - radius; - const top = - y - - radius - - (Number(reactionBubbleBackground.height || 0) || styles.reactionBubbleBackground.height); - - return ( - - - - - - - - - - - - - - - - - - - - - - {name} - - - - ); - }; + const renderItem = ({ item }: { item: Reaction }) => ( + + ); const showScreenStyle = useAnimatedStyle( () => ({ @@ -316,33 +201,38 @@ export const OverlayReactions = (props: OverlayReactionsProps) => { ]} > {title} - `${name}_${index}`} - numColumns={numColumns} - renderItem={renderItem} - scrollEnabled={filteredReactions.length / numColumns > 1} - style={[ - styles.flatListContainer, - flatListContainer, - { - // we show the item height plus a little extra to tease for scrolling if there are more than one row - maxHeight: - itemHeight + (filteredReactions.length / numColumns > 1 ? itemHeight / 4 : 8), - }, - ]} - /> + {!loading && ( + `${name}${id}_${index}`} + numColumns={numColumns} + onEndReached={loadNextPage} + renderItem={renderItem} + scrollEnabled={filteredReactions.length / numColumns > 1} + style={[ + styles.flatListContainer, + flatListContainer, + { + // we show the item height plus a little extra to tease for scrolling if there are more than one row + maxHeight: + itemHeight + (filteredReactions.length / numColumns > 1 ? itemHeight / 4 : 8), + }, + ]} + /> + )} {/* The below view is unseen by the user, we use it to compute the height that the item must be */} - { - setItemHeight(layout.height); - }} - style={[styles.unseenItemContainer, styles.flatListContentContainer]} - > - {renderItem({ item: filteredReactions[0] })} - + {!loading && ( + { + setItemHeight(layout.height); + }} + style={[styles.unseenItemContainer, styles.flatListContentContainer]} + > + {renderItem({ item: filteredReactions[0] })} + + )} ); diff --git a/package/src/components/MessageOverlay/OverlayReactionsItem.tsx b/package/src/components/MessageOverlay/OverlayReactionsItem.tsx new file mode 100644 index 0000000000..cad971046f --- /dev/null +++ b/package/src/components/MessageOverlay/OverlayReactionsItem.tsx @@ -0,0 +1,188 @@ +import React from 'react'; + +import { StyleSheet, Text, View } from 'react-native'; +import Svg, { Circle } from 'react-native-svg'; + +import { ReactionResponse } from 'stream-chat'; + +import { Reaction } from './OverlayReactions'; + +import { useChatContext } from '../../contexts/chatContext/ChatContext'; +import type { MessageOverlayContextValue } from '../../contexts/messageOverlayContext/MessageOverlayContext'; +import { useTheme } from '../../contexts/themeContext/ThemeContext'; +import { Unknown } from '../../icons'; + +import type { DefaultStreamChatGenerics } from '../../types/types'; +import { ReactionData } from '../../utils/utils'; + +export type OverlayReactionsItemProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = Pick, 'OverlayReactionsAvatar'> & { + reaction: Reaction; + supportedReactions: ReactionData[]; +}; + +type ReactionIconProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = Pick, 'type'> & { + pathFill: string; + size: number; + supportedReactions: ReactionData[]; +}; + +const ReactionIcon = ({ pathFill, size, supportedReactions, type }: ReactionIconProps) => { + const Icon = supportedReactions.find((reaction) => reaction.type === type)?.Icon || Unknown; + return ; +}; + +export const OverlayReactionsItem = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>({ + OverlayReactionsAvatar, + reaction, + supportedReactions, +}: OverlayReactionsItemProps) => { + const { id, name, type } = reaction; + const { + theme: { + colors: { accent_blue, black, grey_gainsboro, white }, + overlay: { + reactions: { + avatarContainer, + avatarName, + avatarSize, + radius, + reactionBubble, + reactionBubbleBackground, + reactionBubbleBorderRadius, + }, + }, + }, + } = useTheme(); + const { client } = useChatContext(); + const alignment = client.userID && client.userID === id ? 'right' : 'left'; + const x = avatarSize / 2 - (avatarSize / (radius * 4)) * (alignment === 'left' ? 1 : -1); + const y = avatarSize - radius; + + const left = + alignment === 'left' + ? x - + (Number(reactionBubbleBackground.width || 0) || styles.reactionBubbleBackground.width) + + radius + : x - radius; + const top = + y - + radius - + (Number(reactionBubbleBackground.height || 0) || styles.reactionBubbleBackground.height); + + return ( + + + + + + + + + + + + + + + + + + + + + + {name} + + + + ); +}; + +const styles = StyleSheet.create({ + avatarContainer: { + padding: 8, + }, + avatarInnerContainer: { + alignSelf: 'center', + }, + avatarName: { + flex: 1, + fontSize: 12, + fontWeight: '700', + paddingTop: 6, + textAlign: 'center', + }, + avatarNameContainer: { + alignItems: 'center', + flexDirection: 'row', + flexGrow: 1, + }, + reactionBubble: { + alignItems: 'center', + borderRadius: 24, + justifyContent: 'center', + position: 'absolute', + }, + reactionBubbleBackground: { + borderRadius: 24, + height: 24, + position: 'absolute', + width: 24, + }, +}); diff --git a/package/src/components/MessageOverlay/hooks/useFetchReactions.ts b/package/src/components/MessageOverlay/hooks/useFetchReactions.ts new file mode 100644 index 0000000000..8100e0fa03 --- /dev/null +++ b/package/src/components/MessageOverlay/hooks/useFetchReactions.ts @@ -0,0 +1,88 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { ReactionResponse, ReactionSort } from 'stream-chat'; + +import { useChatContext } from '../../../contexts/chatContext/ChatContext'; +import { getReactionsForFilterSort } from '../../../store/apis/getReactionsforFilterSort'; +import { DefaultStreamChatGenerics } from '../../../types/types'; + +export type UseFetchReactionParams< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = { + limit?: number; + messageId?: string; + reactionType?: string; + sort?: ReactionSort; +}; + +export const useFetchReactions = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>({ + limit = 25, + messageId, + reactionType, + sort, +}: UseFetchReactionParams) => { + const [reactions, setReactions] = useState[]>([]); + const [loading, setLoading] = useState(true); + const [hasNextPage, setHasNextPage] = useState(true); + const [next, setNext] = useState(undefined); + + const { client, enableOfflineSupport } = useChatContext(); + + const sortString = useMemo(() => JSON.stringify(sort), [sort]); + + const loadOfflineReactions = () => { + if (!messageId) return; + const reactionsFromDB = getReactionsForFilterSort({ + currentMessageId: messageId, + filters: reactionType ? { type: reactionType } : {}, + sort, + }); + if (reactionsFromDB) { + setReactions(reactionsFromDB); + setLoading(false); + setHasNextPage(false); + } + }; + + const loadOnlineReactions = async () => { + if (!messageId) return; + const response = await client.queryReactions( + messageId, + reactionType ? { type: reactionType } : {}, + sort, + { limit, next }, + ); + if (response) { + setNext(response.next); + setHasNextPage(response.next !== undefined); + setReactions((prevReactions) => [...prevReactions, ...response.reactions]); + setLoading(false); + } + }; + + const fetchReactions = useCallback(async () => { + try { + if (enableOfflineSupport) { + loadOfflineReactions(); + } else { + await loadOnlineReactions(); + } + } catch (error) { + console.log('Error fetching reactions: ', error); + } + }, [client, messageId, reactionType, sortString, next]); + + const loadNextPage = useCallback(async () => { + if (hasNextPage) { + await fetchReactions(); + } + }, [hasNextPage]); + + useEffect(() => { + fetchReactions(); + }, [messageId, reactionType, sortString]); + + return { loading, loadNextPage, reactions }; +}; diff --git a/package/src/store/apis/getReactions.ts b/package/src/store/apis/getReactions.ts new file mode 100644 index 0000000000..e1cb4cd63a --- /dev/null +++ b/package/src/store/apis/getReactions.ts @@ -0,0 +1,21 @@ +import type { ReactionResponse } from 'stream-chat'; + +import type { DefaultStreamChatGenerics } from '../../types/types'; +import { mapStorableToReaction } from '../mappers/mapStorableToReaction'; +import { QuickSqliteClient } from '../QuickSqliteClient'; +import { TableRowJoinedUser } from '../types'; + +export const getReactions = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>({ + reactions, +}: { + reactions: TableRowJoinedUser<'reactions'>[]; +}): ReactionResponse[] => { + QuickSqliteClient.logger?.('info', 'getReactions', { reactions }); + + // Enrich the channels with state + return reactions.map((reaction) => ({ + ...mapStorableToReaction(reaction), + })); +}; diff --git a/package/src/store/apis/getReactionsforFilterSort.ts b/package/src/store/apis/getReactionsforFilterSort.ts new file mode 100644 index 0000000000..81e0a06a15 --- /dev/null +++ b/package/src/store/apis/getReactionsforFilterSort.ts @@ -0,0 +1,43 @@ +import type { ReactionFilters, ReactionResponse, ReactionSort } from 'stream-chat'; + +import { getReactions } from './getReactions'; +import { selectReactionsForMessages } from './queries/selectReactionsForMessages'; + +import type { DefaultStreamChatGenerics } from '../../types/types'; + +import { QuickSqliteClient } from '../QuickSqliteClient'; + +/** + * Fetches reactions for a message from the database based on the provided filters and sort. + * @param currentMessageId The message ID for which reactions are to be fetched. + * @param filters The filters to be applied while fetching reactions. + * @param sort The sort to be applied while fetching reactions. + */ +export const getReactionsForFilterSort = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>({ + currentMessageId, + filters, + sort, +}: { + currentMessageId: string; + filters?: ReactionFilters; + sort?: ReactionSort; +}): ReactionResponse[] | null => { + if (!filters && !sort) { + console.warn('Please provide the query (filters/sort) to fetch channels from DB'); + return null; + } + + QuickSqliteClient.logger?.('info', 'getReactionsForFilterSort', { filters, sort }); + + const reactions = selectReactionsForMessages([currentMessageId]); + + if (!reactions) return null; + + if (reactions.length === 0) { + return []; + } + + return getReactions({ reactions }); +}; diff --git a/package/src/store/apis/queries/selectReactionsForMessages.ts b/package/src/store/apis/queries/selectReactionsForMessages.ts index 13c48c598d..911193f034 100644 --- a/package/src/store/apis/queries/selectReactionsForMessages.ts +++ b/package/src/store/apis/queries/selectReactionsForMessages.ts @@ -2,6 +2,10 @@ import { QuickSqliteClient } from '../../QuickSqliteClient'; import { tables } from '../../schema'; import type { TableRowJoinedUser } from '../../types'; +/** + * Fetches reactions for a message from the database for messageIds. + * @param messageIds The message IDs for which reactions are to be fetched. + */ export const selectReactionsForMessages = ( messageIds: string[], ): TableRowJoinedUser<'reactions'>[] => { @@ -28,7 +32,7 @@ export const selectReactionsForMessages = ( FROM reactions a LEFT JOIN users b - ON b.id = a.userId + ON b.id = a.userId WHERE a.messageId in (${questionMarks})`, messageIds, ); From a25d90e10a196b7f90f32f2892d2e8be523c746f Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Sun, 2 Jun 2024 21:35:51 +0530 Subject: [PATCH 08/14] fix: improve useFetchReactions hook --- .../MessageOverlay/hooks/useFetchReactions.ts | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/package/src/components/MessageOverlay/hooks/useFetchReactions.ts b/package/src/components/MessageOverlay/hooks/useFetchReactions.ts index 8100e0fa03..3e9c94eaf3 100644 --- a/package/src/components/MessageOverlay/hooks/useFetchReactions.ts +++ b/package/src/components/MessageOverlay/hooks/useFetchReactions.ts @@ -32,37 +32,37 @@ export const useFetchReactions = < const sortString = useMemo(() => JSON.stringify(sort), [sort]); - const loadOfflineReactions = () => { - if (!messageId) return; - const reactionsFromDB = getReactionsForFilterSort({ - currentMessageId: messageId, - filters: reactionType ? { type: reactionType } : {}, - sort, - }); - if (reactionsFromDB) { - setReactions(reactionsFromDB); - setLoading(false); - setHasNextPage(false); - } - }; + const fetchReactions = useCallback(async () => { + const loadOfflineReactions = () => { + if (!messageId) return; + const reactionsFromDB = getReactionsForFilterSort({ + currentMessageId: messageId, + filters: reactionType ? { type: reactionType } : {}, + sort, + }); + if (reactionsFromDB) { + setReactions(reactionsFromDB); + setLoading(false); + setHasNextPage(false); + } + }; - const loadOnlineReactions = async () => { - if (!messageId) return; - const response = await client.queryReactions( - messageId, - reactionType ? { type: reactionType } : {}, - sort, - { limit, next }, - ); - if (response) { - setNext(response.next); - setHasNextPage(response.next !== undefined); - setReactions((prevReactions) => [...prevReactions, ...response.reactions]); - setLoading(false); - } - }; + const loadOnlineReactions = async () => { + if (!messageId) return; + const response = await client.queryReactions( + messageId, + reactionType ? { type: reactionType } : {}, + sort, + { limit, next }, + ); + if (response) { + setNext(response.next); + setHasNextPage(response.next !== undefined); + setReactions((prevReactions) => [...prevReactions, ...response.reactions]); + setLoading(false); + } + }; - const fetchReactions = useCallback(async () => { try { if (enableOfflineSupport) { loadOfflineReactions(); From 1fd162ee7caed7a129eb947e0ea342fbd21368f1 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Mon, 3 Jun 2024 10:30:34 +0530 Subject: [PATCH 09/14] docs: old docs lint fix --- docusaurus/docs/reactnative/basics/client.mdx | 8 ++++---- .../state-and-offline-support/state-overview.mdx | 8 ++++---- .../version-3.x.x/basics/client.mdx | 8 ++++---- .../basics/stream_chat_with_navigation.mdx | 11 +++++------ .../version-4.x.x/basics/client.mdx | 8 ++++---- .../basics/stream_chat_with_navigation.mdx | 11 +++++------ 6 files changed, 26 insertions(+), 28 deletions(-) diff --git a/docusaurus/docs/reactnative/basics/client.mdx b/docusaurus/docs/reactnative/basics/client.mdx index 3270c7d674..68bc6254f7 100644 --- a/docusaurus/docs/reactnative/basics/client.mdx +++ b/docusaurus/docs/reactnative/basics/client.mdx @@ -31,11 +31,11 @@ const client = StreamChat.getInstance('api_key'); **Usage of `StreamChat.getInstance()` available since stream-chat@2.12.0.**
-This new Singleton pattern allows you to instantiate a unique StreamChat client, i.e create a StreamChat instance and retrieve -it wherever you need it on your app to perform API calls. After calling it once, any following +This new Singleton pattern allows you to instantiate a unique StreamChat client, i.e create a StreamChat instance and +retrieve it wherever you need it on your app to perform API calls. After calling it once, any following getInstance - call will return the initial StreamChat instance. This will prevent you from accidentally creating multiple StreamChat -instances, opening multiple WebSockets, and driving up your concurrent connections unnecessarily. + call will return the initial StreamChat instance. This will prevent you from accidentally creating multiple +StreamChat instances, opening multiple WebSockets, and driving up your concurrent connections unnecessarily.

diff --git a/docusaurus/docs/reactnative/state-and-offline-support/state-overview.mdx b/docusaurus/docs/reactnative/state-and-offline-support/state-overview.mdx index 0a23f6e12f..f9e49d34c6 100644 --- a/docusaurus/docs/reactnative/state-and-offline-support/state-overview.mdx +++ b/docusaurus/docs/reactnative/state-and-offline-support/state-overview.mdx @@ -30,11 +30,11 @@ const client = StreamChat.getInstance('api_key'); **Usage of `StreamChat.getInstance()` available since stream-chat@2.12.0.**
-This new Singleton pattern allows you to instantiate a unique StreamChat client, i.e create a StreamChat instance and retrieve -it wherever you need it on your app to perform API calls. After calling it once, any following +This new Singleton pattern allows you to instantiate a unique StreamChat client, i.e create a StreamChat instance and +retrieve it wherever you need it on your app to perform API calls. After calling it once, any following getInstance - call will return the initial StreamChat instance. This will prevent you from accidentally creating multiple StreamChat -instances, opening multiple WebSockets, and driving up your concurrent connections unnecessarily. + call will return the initial StreamChat instance. This will prevent you from accidentally creating multiple +StreamChat instances, opening multiple WebSockets, and driving up your concurrent connections unnecessarily.

diff --git a/docusaurus/reactnative_versioned_docs/version-3.x.x/basics/client.mdx b/docusaurus/reactnative_versioned_docs/version-3.x.x/basics/client.mdx index 6fe2d090d5..94c28a434e 100644 --- a/docusaurus/reactnative_versioned_docs/version-3.x.x/basics/client.mdx +++ b/docusaurus/reactnative_versioned_docs/version-3.x.x/basics/client.mdx @@ -31,11 +31,11 @@ const client = StreamChat.getInstance('api_key'); **Usage of `StreamChat.getInstance()` available since stream-chat@2.12.0.**
-This new Singleton pattern allows you to instantiate a unique StreamChat client, i.e create a StreamChat instance and retrieve -it wherever you need it on your app to perform API calls. After calling it once, any following +This new Singleton pattern allows you to instantiate a unique StreamChat client, i.e create a StreamChat instance and +retrieve it wherever you need it on your app to perform API calls. After calling it once, any following getInstance - call will return the initial StreamChat instance. This will prevent you from accidentally creating multiple StreamChat -instances, opening multiple WebSockets, and driving up your concurrent connections unnecessarily. + call will return the initial StreamChat instance. This will prevent you from accidentally creating multiple +StreamChat instances, opening multiple WebSockets, and driving up your concurrent connections unnecessarily.

diff --git a/docusaurus/reactnative_versioned_docs/version-3.x.x/basics/stream_chat_with_navigation.mdx b/docusaurus/reactnative_versioned_docs/version-3.x.x/basics/stream_chat_with_navigation.mdx index be395026aa..1ab8a787ae 100644 --- a/docusaurus/reactnative_versioned_docs/version-3.x.x/basics/stream_chat_with_navigation.mdx +++ b/docusaurus/reactnative_versioned_docs/version-3.x.x/basics/stream_chat_with_navigation.mdx @@ -21,13 +21,12 @@ The guidance provided makes the assumption you are using [React Navigation](http createNativeStackNavigator uses the native APIs UINavigationController on iOS and Fragment - on Android. The OverlayProvider needs to exist in a view that can render content in front of the chat -screen. Therefore using a fullScreenModal with createNativeStackNavigator, which uses + on Android. The OverlayProvider needs to exist in a view that can render content in front of the +chat screen. Therefore using a fullScreenModal with createNativeStackNavigator, which uses UIModalPresentationFullScreen - on iOS and modal on Android, to render your chat screen will leave the - OverlayProvider - rendered behind the chat. If you are having issues we suggest you get in touch with support and we can find a -solution to your specific navigation arrangement. + on iOS and modal on Android, to render your chat screen will leave the OverlayProvider rendered +behind the chat. If you are having issues we suggest you get in touch with support and we can find a solution to your +specific navigation arrangement. ::: diff --git a/docusaurus/reactnative_versioned_docs/version-4.x.x/basics/client.mdx b/docusaurus/reactnative_versioned_docs/version-4.x.x/basics/client.mdx index 6fe2d090d5..94c28a434e 100644 --- a/docusaurus/reactnative_versioned_docs/version-4.x.x/basics/client.mdx +++ b/docusaurus/reactnative_versioned_docs/version-4.x.x/basics/client.mdx @@ -31,11 +31,11 @@ const client = StreamChat.getInstance('api_key'); **Usage of `StreamChat.getInstance()` available since stream-chat@2.12.0.**
-This new Singleton pattern allows you to instantiate a unique StreamChat client, i.e create a StreamChat instance and retrieve -it wherever you need it on your app to perform API calls. After calling it once, any following +This new Singleton pattern allows you to instantiate a unique StreamChat client, i.e create a StreamChat instance and +retrieve it wherever you need it on your app to perform API calls. After calling it once, any following getInstance - call will return the initial StreamChat instance. This will prevent you from accidentally creating multiple StreamChat -instances, opening multiple WebSockets, and driving up your concurrent connections unnecessarily. + call will return the initial StreamChat instance. This will prevent you from accidentally creating multiple +StreamChat instances, opening multiple WebSockets, and driving up your concurrent connections unnecessarily.

diff --git a/docusaurus/reactnative_versioned_docs/version-4.x.x/basics/stream_chat_with_navigation.mdx b/docusaurus/reactnative_versioned_docs/version-4.x.x/basics/stream_chat_with_navigation.mdx index 5f3c5bf659..0f78f099aa 100644 --- a/docusaurus/reactnative_versioned_docs/version-4.x.x/basics/stream_chat_with_navigation.mdx +++ b/docusaurus/reactnative_versioned_docs/version-4.x.x/basics/stream_chat_with_navigation.mdx @@ -21,13 +21,12 @@ The guidance provided makes the assumption you are using [React Navigation](http createNativeStackNavigator uses the native APIs UINavigationController on iOS and Fragment - on Android. The OverlayProvider needs to exist in a view that can render content in front of the chat -screen. Therefore using a fullScreenModal with createNativeStackNavigator, which uses + on Android. The OverlayProvider needs to exist in a view that can render content in front of the +chat screen. Therefore using a fullScreenModal with createNativeStackNavigator, which uses UIModalPresentationFullScreen - on iOS and modal on Android, to render your chat screen will leave the - OverlayProvider - rendered behind the chat. If you are having issues we suggest you get in touch with support and we can find a -solution to your specific navigation arrangement. + on iOS and modal on Android, to render your chat screen will leave the OverlayProvider rendered +behind the chat. If you are having issues we suggest you get in touch with support and we can find a solution to your +specific navigation arrangement. ::: From 5b6f44cdad63f85badaf940d387d7a7d18879abe Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Thu, 6 Jun 2024 20:45:38 +0530 Subject: [PATCH 10/14] fix: remove repeated code --- package/src/utils/addReactionToLocalState.ts | 27 -------------------- 1 file changed, 27 deletions(-) diff --git a/package/src/utils/addReactionToLocalState.ts b/package/src/utils/addReactionToLocalState.ts index ae3bd14085..2f8c3c10e1 100644 --- a/package/src/utils/addReactionToLocalState.ts +++ b/package/src/utils/addReactionToLocalState.ts @@ -116,33 +116,6 @@ export const addReactionToLocalState = < }; } } - } else { - 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, - }; - } - } if (!message.reaction_groups) { message.reaction_groups = { From 6a885b2ee47a242d9492c6e197920ab356fc38e9 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Thu, 6 Jun 2024 20:47:21 +0530 Subject: [PATCH 11/14] fix: remove repeated code --- package/src/utils/addReactionToLocalState.ts | 54 -------------------- 1 file changed, 54 deletions(-) diff --git a/package/src/utils/addReactionToLocalState.ts b/package/src/utils/addReactionToLocalState.ts index 2f8c3c10e1..4a6fa22ff3 100644 --- a/package/src/utils/addReactionToLocalState.ts +++ b/package/src/utils/addReactionToLocalState.ts @@ -89,60 +89,6 @@ export const addReactionToLocalState = < }; } } - - 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, - }; - } - } - - 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]; From 7e55edd8cdc6fc0fab8a92a57f24abca6043855c Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Fri, 7 Jun 2024 12:42:37 +0530 Subject: [PATCH 12/14] fix: useFetchReactions hook --- .../MessageOverlay/hooks/useFetchReactions.ts | 11 ++++------- package/src/store/apis/updateReaction.ts | 10 ---------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/package/src/components/MessageOverlay/hooks/useFetchReactions.ts b/package/src/components/MessageOverlay/hooks/useFetchReactions.ts index 3e9c94eaf3..8f20815449 100644 --- a/package/src/components/MessageOverlay/hooks/useFetchReactions.ts +++ b/package/src/components/MessageOverlay/hooks/useFetchReactions.ts @@ -18,14 +18,13 @@ export type UseFetchReactionParams< export const useFetchReactions = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ - limit = 25, + limit = 2, messageId, reactionType, sort, }: UseFetchReactionParams) => { const [reactions, setReactions] = useState[]>([]); const [loading, setLoading] = useState(true); - const [hasNextPage, setHasNextPage] = useState(true); const [next, setNext] = useState(undefined); const { client, enableOfflineSupport } = useChatContext(); @@ -43,7 +42,6 @@ export const useFetchReactions = < if (reactionsFromDB) { setReactions(reactionsFromDB); setLoading(false); - setHasNextPage(false); } }; @@ -57,7 +55,6 @@ export const useFetchReactions = < ); if (response) { setNext(response.next); - setHasNextPage(response.next !== undefined); setReactions((prevReactions) => [...prevReactions, ...response.reactions]); setLoading(false); } @@ -72,13 +69,13 @@ export const useFetchReactions = < } catch (error) { console.log('Error fetching reactions: ', error); } - }, [client, messageId, reactionType, sortString, next]); + }, [client, messageId, reactionType, sortString, next, enableOfflineSupport]); const loadNextPage = useCallback(async () => { - if (hasNextPage) { + if (next) { await fetchReactions(); } - }, [hasNextPage]); + }, [fetchReactions]); useEffect(() => { fetchReactions(); diff --git a/package/src/store/apis/updateReaction.ts b/package/src/store/apis/updateReaction.ts index cdb2c8d1b6..c8f35d4daf 100644 --- a/package/src/store/apis/updateReaction.ts +++ b/package/src/store/apis/updateReaction.ts @@ -34,15 +34,6 @@ export const updateReaction = ({ }), ); - let updatedReactionCounts: string | undefined; - - let updatedReactionGroups: string | undefined; - if (message.reaction_groups) { - const { reactionGroups } = mapMessageToStorable(message); - updatedReactionGroups = reactionGroups; - queries.push(createUpdateQuery('messages', { reactionGroups }, { id: message.id })); - } - let updatedReactionGroups: string | undefined; if (message.reaction_groups) { const { reactionGroups } = mapMessageToStorable(message); @@ -54,7 +45,6 @@ export const updateReaction = ({ addedUser: storableUser, flush, updatedReaction: storableReaction, - updatedReactionCounts, updatedReactionGroups, }); From 550c3f74f21e686758c11a43c71a816eed9eb74d Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Mon, 10 Jun 2024 11:38:49 +0530 Subject: [PATCH 13/14] fix: reactions query limit --- .../src/components/MessageOverlay/hooks/useFetchReactions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/src/components/MessageOverlay/hooks/useFetchReactions.ts b/package/src/components/MessageOverlay/hooks/useFetchReactions.ts index 8f20815449..62a0290f1b 100644 --- a/package/src/components/MessageOverlay/hooks/useFetchReactions.ts +++ b/package/src/components/MessageOverlay/hooks/useFetchReactions.ts @@ -18,7 +18,7 @@ export type UseFetchReactionParams< export const useFetchReactions = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ - limit = 2, + limit = 25, messageId, reactionType, sort, From 244f1d37ea13c4bc21fb28943692e434b845bb97 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Mon, 10 Jun 2024 15:40:49 +0530 Subject: [PATCH 14/14] fix: use usememo in the reactions array --- .../MessageOverlay/OverlayReactions.tsx | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/package/src/components/MessageOverlay/OverlayReactions.tsx b/package/src/components/MessageOverlay/OverlayReactions.tsx index c295dcde84..b71675bce1 100644 --- a/package/src/components/MessageOverlay/OverlayReactions.tsx +++ b/package/src/components/MessageOverlay/OverlayReactions.tsx @@ -1,8 +1,10 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { StyleSheet, Text, useWindowDimensions, View, ViewStyle } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; import Animated, { interpolate, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; +import { ReactionSortBase } from 'stream-chat'; + import { useFetchReactions } from './hooks/useFetchReactions'; import { OverlayReactionsItem } from './OverlayReactionsItem'; @@ -92,6 +94,10 @@ export type OverlayReactionsProps< supportedReactions?: ReactionData[]; }; +const sort: ReactionSortBase = { + created_at: -1, +}; + /** * OverlayReactions - A high level component which implements all the logic required for message overlay reactions */ @@ -114,18 +120,21 @@ export const OverlayReactions = (props: OverlayReactionsProps) => { reactions: fetchedReactions, } = useFetchReactions({ messageId, - sort: { created_at: -1 }, + sort, }); - const reactions = - propReactions || - (fetchedReactions.map((reaction) => ({ - alignment: 'left', - id: reaction.user?.id, - image: reaction.user?.image, - name: reaction.user?.name, - type: reaction.type, - })) as Reaction[]); + const reactions = useMemo( + () => + propReactions || + (fetchedReactions.map((reaction) => ({ + alignment: 'left', + id: reaction.user?.id, + image: reaction.user?.image, + name: reaction.user?.name, + type: reaction.type, + })) as Reaction[]), + [propReactions, fetchedReactions], + ); const { theme: {