Skip to content

Commit

Permalink
feat: use queryReactions to fetch and show reactions in OverlayReacti…
Browse files Browse the repository at this point in the history
…ons. (#2532)

* fix: reaction list reactions sorting order based on created_at

* fix: show reactions using reactions_group

* fix: tests

* fix: useProcessReactions hook

* fix: useProcessReactions hook and Channel message spread

* feat: use queryReactions API to query and show reactions in OverlayReactions

* feat: use queryReactions API to query and show reactions in OverlayReactions

* fix: improve useFetchReactions hook

* docs: old docs lint fix

* fix: remove repeated code

* fix: remove repeated code

* fix: useFetchReactions hook

* fix: reactions query limit

* fix: use usememo in the reactions array
  • Loading branch information
khushal87 authored Jun 10, 2024
1 parent 6dbc9bf commit d7c8243
Show file tree
Hide file tree
Showing 10 changed files with 432 additions and 227 deletions.
14 changes: 11 additions & 3 deletions docusaurus/docs/reactnative/ui-components/overlay-reactions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ This is the default component provided to the prop [`OverlayReactions`](../core-

<Alignment />

### `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.
Expand All @@ -31,9 +39,9 @@ const reactions = message.latest_reactions.map(reaction => ({
}));
```

| Type |
| ----- |
| Array |
| Type | Default |
| ---------------------- | ----------- |
| `Array` \| `undefined` | `undefined` |

### <div class="label description required">required</div> `showScreen` {#showscreen}

Expand Down
21 changes: 4 additions & 17 deletions package/src/components/MessageOverlay/MessageOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -464,26 +461,16 @@ const MessageOverlayWithContext = <
message={message}
/>
)}
{!!messageReactionTitle &&
message.latest_reactions &&
message.latest_reactions.length > 0 ? (
{!!messageReactionTitle && (
<OverlayReactions
alignment={alignment}
messageId={message.id}
OverlayReactionsAvatar={OverlayReactionsAvatar}
reactions={
message.latest_reactions.map((reaction) => ({
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}
)}
</View>
)}
</Animated.View>
Expand Down
251 changes: 75 additions & 176 deletions package/src/components/MessageOverlay/OverlayReactions.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,7 +17,6 @@ import {
LoveReaction,
ThumbsDownReaction,
ThumbsUpReaction,
Unknown,
WutReaction,
} from '../../icons';

Expand All @@ -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,
Expand All @@ -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',
Expand Down Expand Up @@ -109,58 +86,62 @@ export type Reaction = {
export type OverlayReactionsProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
> = Pick<MessageOverlayContextValue<StreamChatGenerics>, 'OverlayReactionsAvatar'> & {
reactions: Reaction[];
showScreen: Animated.SharedValue<number>;
title: string;
alignment?: Alignment;
messageId?: string;
reactions?: Reaction[];
supportedReactions?: ReactionData[];
};

type ReactionIconProps = Pick<Reaction, '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 <Icon height={size} pathFill={pathFill} width={size} />;
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();
Expand All @@ -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 (
<View style={[styles.avatarContainer, avatarContainer]}>
<View style={styles.avatarInnerContainer}>
<OverlayReactionsAvatar reaction={item} size={avatarSize} />
<View style={[StyleSheet.absoluteFill]}>
<Svg>
<Circle
cx={x - (radius * 2 - radius / 4) * (alignment === 'left' ? 1 : -1)}
cy={y - radius * 2 - radius / 4}
fill={alignment === 'left' ? grey_gainsboro : white}
r={radius * 2}
stroke={alignment === 'left' ? white : grey_gainsboro}
strokeWidth={radius / 2}
/>
<Circle
cx={x}
cy={y}
fill={alignment === 'left' ? grey_gainsboro : white}
r={radius}
stroke={alignment === 'left' ? white : grey_gainsboro}
strokeWidth={radius / 2}
/>
</Svg>
<View
style={[
styles.reactionBubbleBackground,
{
backgroundColor: alignment === 'left' ? grey_gainsboro : white,
borderColor: alignment === 'left' ? white : grey_gainsboro,
borderWidth: radius / 2,
left,
top,
},
reactionBubbleBackground,
]}
/>
<View style={[StyleSheet.absoluteFill]}>
<Svg>
<Circle
cx={x - (radius * 2 - radius / 4) * (alignment === 'left' ? 1 : -1)}
cy={y - radius * 2 - radius / 4}
fill={alignment === 'left' ? grey_gainsboro : white}
r={radius * 2 - radius / 2}
/>
</Svg>
</View>
<View
style={[
styles.reactionBubble,
{
backgroundColor: alignment === 'left' ? grey_gainsboro : white,
height:
(reactionBubbleBorderRadius || styles.reactionBubble.borderRadius) - radius / 2,
left,
top,
width:
(reactionBubbleBorderRadius || styles.reactionBubble.borderRadius) - radius / 2,
},
reactionBubble,
]}
>
<ReactionIcon
pathFill={accent_blue}
size={(reactionBubbleBorderRadius || styles.reactionBubble.borderRadius) / 2}
supportedReactions={supportedReactions}
type={type}
/>
</View>
</View>
</View>
<View style={styles.avatarNameContainer}>
<Text numberOfLines={2} style={[styles.avatarName, { color: black }, avatarName]}>
{name}
</Text>
</View>
</View>
);
};
const renderItem = ({ item }: { item: Reaction }) => (
<OverlayReactionsItem
OverlayReactionsAvatar={OverlayReactionsAvatar}
reaction={item}
supportedReactions={supportedReactions}
/>
);

const showScreenStyle = useAnimatedStyle<ViewStyle>(
() => ({
Expand Down Expand Up @@ -316,33 +210,38 @@ export const OverlayReactions = (props: OverlayReactionsProps) => {
]}
>
<Text style={[styles.title, { color: black }, titleStyle]}>{title}</Text>
<FlatList
contentContainerStyle={styles.flatListContentContainer}
data={filteredReactions}
key={numColumns}
keyExtractor={({ name }, index) => `${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 && (
<FlatList
contentContainerStyle={styles.flatListContentContainer}
data={filteredReactions}
key={numColumns}
keyExtractor={({ id, name }, index) => `${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 */}
<View
onLayout={({ nativeEvent: { layout } }) => {
setItemHeight(layout.height);
}}
style={[styles.unseenItemContainer, styles.flatListContentContainer]}
>
{renderItem({ item: filteredReactions[0] })}
</View>
{!loading && (
<View
onLayout={({ nativeEvent: { layout } }) => {
setItemHeight(layout.height);
}}
style={[styles.unseenItemContainer, styles.flatListContentContainer]}
>
{renderItem({ item: filteredReactions[0] })}
</View>
)}
</Animated.View>
</>
);
Expand Down
Loading

0 comments on commit d7c8243

Please sign in to comment.