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/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}
+ )}
)}
diff --git a/package/src/components/MessageOverlay/OverlayReactions.tsx b/package/src/components/MessageOverlay/OverlayReactions.tsx
index 7c443fa59d..b71675bce1 100644
--- a/package/src/components/MessageOverlay/OverlayReactions.tsx
+++ b/package/src/components/MessageOverlay/OverlayReactions.tsx
@@ -1,8 +1,13 @@
-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 Svg, { Circle } from 'react-native-svg';
+
+import { ReactionSortBase } from 'stream-chat';
+
+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 +17,6 @@ import {
LoveReaction,
ThumbsDownReaction,
ThumbsUpReaction,
- Unknown,
WutReaction,
} from '../../icons';
@@ -23,21 +27,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 +41,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 +86,62 @@ 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 ;
+const sort: ReactionSortBase = {
+ created_at: -1,
};
/**
* 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,
+ });
+
+ 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: {
- 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 +166,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 +210,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..62a0290f1b
--- /dev/null
+++ b/package/src/components/MessageOverlay/hooks/useFetchReactions.ts
@@ -0,0 +1,85 @@
+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 [next, setNext] = useState(undefined);
+
+ const { client, enableOfflineSupport } = useChatContext();
+
+ const sortString = useMemo(() => JSON.stringify(sort), [sort]);
+
+ 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);
+ }
+ };
+
+ const loadOnlineReactions = async () => {
+ if (!messageId) return;
+ const response = await client.queryReactions(
+ messageId,
+ reactionType ? { type: reactionType } : {},
+ sort,
+ { limit, next },
+ );
+ if (response) {
+ setNext(response.next);
+ setReactions((prevReactions) => [...prevReactions, ...response.reactions]);
+ setLoading(false);
+ }
+ };
+
+ try {
+ if (enableOfflineSupport) {
+ loadOfflineReactions();
+ } else {
+ await loadOnlineReactions();
+ }
+ } catch (error) {
+ console.log('Error fetching reactions: ', error);
+ }
+ }, [client, messageId, reactionType, sortString, next, enableOfflineSupport]);
+
+ const loadNextPage = useCallback(async () => {
+ if (next) {
+ await fetchReactions();
+ }
+ }, [fetchReactions]);
+
+ 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,
);
diff --git a/package/src/store/apis/updateReaction.ts b/package/src/store/apis/updateReaction.ts
index d7a797cce7..c8f35d4daf 100644
--- a/package/src/store/apis/updateReaction.ts
+++ b/package/src/store/apis/updateReaction.ts
@@ -34,8 +34,6 @@ export const updateReaction = ({
}),
);
- let updatedReactionCounts: string | undefined;
-
let updatedReactionGroups: string | undefined;
if (message.reaction_groups) {
const { reactionGroups } = mapMessageToStorable(message);
@@ -47,7 +45,6 @@ export const updateReaction = ({
addedUser: storableUser,
flush,
updatedReaction: storableReaction,
- updatedReactionCounts,
updatedReactionGroups,
});
diff --git a/package/src/utils/addReactionToLocalState.ts b/package/src/utils/addReactionToLocalState.ts
index b3a81dd613..4a6fa22ff3 100644
--- a/package/src/utils/addReactionToLocalState.ts
+++ b/package/src/utils/addReactionToLocalState.ts
@@ -63,33 +63,6 @@ export const addReactionToLocalState = <
message.reaction_groups[currentReaction.type].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,
- };
- }
- }
- } else {
if (!message.reaction_groups) {
message.reaction_groups = {
[reactionType]: {