diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx
index fc46d0ec9..556ec532c 100644
--- a/components/Chat/Chat.tsx
+++ b/components/Chat/Chat.tsx
@@ -52,6 +52,7 @@ import { getProfile, getProfileData } from "../../utils/profile";
import { UUID_REGEX } from "../../utils/regex";
import { isContentType } from "../../utils/xmtpRN/contentTypes";
import { Recommendation } from "../Recommendations/Recommendation";
+import { MessageReactionsDrawer } from "./Message/MessageReactions/MessageReactionsDrawer/MessageReactionsDrawer";
const usePeerSocials = () => {
const conversation = useConversationContext("conversation");
@@ -430,81 +431,86 @@ export function Chat() {
}, [onReadyToFocus]);
return (
-
-
- {conversation && listArray.length > 0 && !isBlockedPeer && (
- {
- if (r) {
- messageListRef.current = r;
+ <>
+
+
+ {conversation && listArray.length > 0 && !isBlockedPeer && (
+ {
+ if (r) {
+ messageListRef.current = r;
+ }
+ }}
+ keyboardDismissMode="interactive"
+ automaticallyAdjustContentInsets={false}
+ contentInsetAdjustmentBehavior="never"
+ // Causes a glitch on Android, no sure we need it for now
+ // maintainVisibleContentPosition={{
+ // minIndexForVisible: 0,
+ // autoscrollToTopThreshold: 100,
+ // }}
+ estimatedListSize={
+ isSplitScreen ? undefined : Dimensions.get("screen")
}
- }}
- keyboardDismissMode="interactive"
- automaticallyAdjustContentInsets={false}
- contentInsetAdjustmentBehavior="never"
- // Causes a glitch on Android, no sure we need it for now
- // maintainVisibleContentPosition={{
- // minIndexForVisible: 0,
- // autoscrollToTopThreshold: 100,
- // }}
- estimatedListSize={
- isSplitScreen ? undefined : Dimensions.get("screen")
- }
- inverted
- keyExtractor={keyExtractor}
- getItemType={getItemType(framesStore)}
- keyboardShouldPersistTaps="handled"
- estimatedItemSize={80}
- // Size glitch on Android
- showsVerticalScrollIndicator={Platform.OS === "ios"}
- pointerEvents="auto"
- ListFooterComponent={ListFooterComponent}
- />
+ inverted
+ keyExtractor={keyExtractor}
+ getItemType={getItemType(framesStore)}
+ keyboardShouldPersistTaps="handled"
+ estimatedItemSize={80}
+ // Size glitch on Android
+ showsVerticalScrollIndicator={Platform.OS === "ios"}
+ pointerEvents="auto"
+ ListFooterComponent={ListFooterComponent}
+ />
+ )}
+ {showPlaceholder && !conversation?.isGroup && (
+
+ )}
+ {showPlaceholder && conversation?.isGroup && (
+
+ )}
+ {conversation?.isGroup ? : }
+
+ {showChatInput && (
+ <>
+
+ {!transactionMode && }
+ {transactionMode && }
+
+
+ >
)}
- {showPlaceholder && !conversation?.isGroup && (
-
- )}
- {showPlaceholder && conversation?.isGroup && (
-
- )}
- {conversation?.isGroup ? : }
-
- {showChatInput && (
- <>
-
- {!transactionMode && }
- {transactionMode && }
-
-
- >
- )}
-
+
+
+ >
);
}
diff --git a/components/Chat/Message/Message.tsx b/components/Chat/Message/Message.tsx
index 82979ac00..e498045f2 100644
--- a/components/Chat/Message/Message.tsx
+++ b/components/Chat/Message/Message.tsx
@@ -29,7 +29,7 @@ import Animated, {
} from "react-native-reanimated";
import ChatMessageActions from "./MessageActions";
-import { ChatMessageReactions } from "./MessageReactions";
+import { ChatMessageReactions } from "./MessageReactions/MessageReactions";
import MessageStatus from "./MessageStatus";
import {
currentAccount,
diff --git a/components/Chat/Message/MessageReactions.tsx b/components/Chat/Message/MessageReactions.tsx
deleted file mode 100644
index 3c6364835..000000000
--- a/components/Chat/Message/MessageReactions.tsx
+++ /dev/null
@@ -1,270 +0,0 @@
-import { useCurrentAccount } from "@data/store/accountsStore";
-import { createBottomSheetModalRef } from "@design-system/BottomSheet/BottomSheet.utils";
-import { BottomSheetContentContainer } from "@design-system/BottomSheet/BottomSheetContentContainer";
-import { BottomSheetHeader } from "@design-system/BottomSheet/BottomSheetHeader";
-import { BottomSheetModal } from "@design-system/BottomSheet/BottomSheetModal";
-import { HStack } from "@design-system/HStack";
-import { ScrollView } from "@design-system/ScrollView";
-import { Text } from "@design-system/Text";
-import { VStack } from "@design-system/VStack";
-import {
- BottomSheetBackdrop,
- BottomSheetScrollView,
-} from "@gorhom/bottom-sheet";
-import { BottomSheetDefaultBackdropProps } from "@gorhom/bottom-sheet/lib/typescript/components/bottomSheetBackdrop/types";
-import { useAppTheme } from "@theme/useAppTheme";
-import { MessageReaction } from "@utils/reactions";
-import { memo, useCallback, useEffect, useMemo, useState } from "react";
-import { StyleSheet, TouchableHighlight } from "react-native";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
-
-import { MessageToDisplay } from "./Message";
-
-const MAX_REACTION_EMOJIS_SHOWN = 3;
-
-type Props = {
- message: MessageToDisplay;
- reactions: {
- [senderAddress: string]: MessageReaction[];
- };
-};
-
-type ReactionDetails = {
- content: string;
- count: number;
- userReacted: boolean;
- reactors: string[];
- firstReactionTime: number;
-};
-
-type RolledUpReactions = {
- emojis: string[];
- totalReactions: number;
- userReacted: boolean;
- details: { [content: string]: ReactionDetails };
-};
-
-export const ChatMessageReactions = memo(
- ({ message, reactions }: Props) => {
- const styles = useStyles();
- const { top } = useSafeAreaInsets();
- const { theme } = useAppTheme();
- const userAddress = useCurrentAccount();
- const bottomSheetModalRef = createBottomSheetModalRef();
- const [isBottomSheetVisible, setBottomSheetVisible] = useState(false);
-
- const openReactionsDrawer = () => {
- setBottomSheetVisible(true);
- };
-
- const onReactionsDrawerDismiss = () => {
- setBottomSheetVisible(false);
- };
-
- const onReactionDrawerChange = useCallback(
- (index: number) => {
- // Dismiss when index 0 (open but not visible)
- if (index === 0) {
- setBottomSheetVisible(false);
- bottomSheetModalRef.current?.dismiss();
- }
- },
- [bottomSheetModalRef]
- );
-
- useEffect(() => {
- if (isBottomSheetVisible) {
- bottomSheetModalRef.current?.present();
- }
- }, [isBottomSheetVisible, bottomSheetModalRef]);
-
- const renderBackdrop = useCallback(
- (props: BottomSheetDefaultBackdropProps) => (
-
- ),
- [theme]
- );
-
- const rolledUpReactions: RolledUpReactions = useMemo(() => {
- const details: { [content: string]: ReactionDetails } = {};
- let totalReactions = 0;
- let userReacted = false;
-
- Object.values(reactions).forEach((reactionArray) => {
- reactionArray.forEach((reaction) => {
- if (!details[reaction.content]) {
- details[reaction.content] = {
- content: reaction.content,
- count: 0,
- userReacted: false,
- reactors: [],
- firstReactionTime: reaction.sent,
- };
- }
- details[reaction.content].count++;
- details[reaction.content].reactors.push(reaction.senderAddress);
- if (
- reaction.senderAddress.toLowerCase() === userAddress?.toLowerCase()
- ) {
- details[reaction.content].userReacted = true;
- userReacted = true;
- }
- // Keep track of the earliest reaction time for this emoji
- details[reaction.content].firstReactionTime = Math.min(
- details[reaction.content].firstReactionTime,
- reaction.sent
- );
- totalReactions++;
- });
- });
-
- // Sort by the number of reactors in descending order
- const sortedReactions = Object.values(details)
- .sort((a, b) => b.reactors.length - a.reactors.length)
- .slice(0, MAX_REACTION_EMOJIS_SHOWN)
- .map((reaction) => reaction.content);
-
- return { emojis: sortedReactions, totalReactions, userReacted, details };
- }, [reactions, userAddress]);
-
- if (rolledUpReactions.totalReactions === 0) return null;
-
- return (
-
-
-
-
- {rolledUpReactions.emojis.map((emoji, index) => (
- {emoji}
- ))}
-
- {rolledUpReactions.totalReactions > 1 && (
-
- {rolledUpReactions.totalReactions}
-
- )}
-
-
- {isBottomSheetVisible && (
-
-
-
-
-
- All {rolledUpReactions.totalReactions}
- {Object.entries(rolledUpReactions.details).map(
- ([emoji, details]) => (
-
-
- {emoji}
- {details.count}
-
-
- )
- )}
-
-
-
-
-
- )}
-
- );
- },
- (prevProps, nextProps) => {
- if (prevProps.message.id !== nextProps.message.id) {
- return false;
- }
- if (prevProps.message.lastUpdateAt !== nextProps.message.lastUpdateAt) {
- return false;
- }
- return true;
- }
-);
-
-const useStyles = () => {
- const { theme } = useAppTheme();
-
- return StyleSheet.create({
- reactionsWrapper: {
- flexDirection: "row",
- flexWrap: "wrap",
- },
- reactionButton: {
- flexDirection: "row",
- alignItems: "center",
- paddingHorizontal: theme.spacing.xs,
- paddingVertical: theme.spacing.xxs,
- borderRadius: theme.borderRadius.sm,
- borderWidth: theme.borderWidth.sm,
- borderColor: theme.colors.border.subtle,
- },
- emojiContainer: {
- flexDirection: "row",
- flexWrap: "wrap",
- gap: theme.spacing.xxxs,
- },
- reactorCount: {
- marginLeft: theme.spacing.xxxs,
- color: theme.colors.text.secondary,
- },
- });
-};
diff --git a/components/Chat/Message/MessageReactions/MessageReactions.tsx b/components/Chat/Message/MessageReactions/MessageReactions.tsx
new file mode 100644
index 000000000..8fd7290f5
--- /dev/null
+++ b/components/Chat/Message/MessageReactions/MessageReactions.tsx
@@ -0,0 +1,169 @@
+import { useCurrentAccount } from "@data/store/accountsStore";
+import { HStack } from "@design-system/HStack";
+import { Text } from "@design-system/Text";
+import { VStack } from "@design-system/VStack";
+import { useAppTheme } from "@theme/useAppTheme";
+import { memo, useCallback, useMemo } from "react";
+import { StyleSheet, TouchableHighlight } from "react-native";
+
+import { MessageToDisplay } from "../Message";
+import {
+ MessageReactions,
+ ReactionDetails,
+ RolledUpReactions,
+} from "./MessageReactions.types";
+import { openMessageReactionsDrawer } from "./MessageReactionsDrawer/MessageReactionsDrawer.service";
+
+type Props = {
+ message: MessageToDisplay;
+ reactions: MessageReactions;
+};
+
+export const ChatMessageReactions = memo(
+ ({ message, reactions }: Props) => {
+ const styles = useStyles();
+ const { theme } = useAppTheme();
+ const userAddress = useCurrentAccount();
+
+ const rolledUpReactions = useMessageReactionsRolledUp({
+ reactions,
+ userAddress: userAddress!, // ! If we are here, the user is logged in
+ });
+
+ const handlePress = useCallback(() => {
+ openMessageReactionsDrawer(rolledUpReactions);
+ }, [rolledUpReactions]);
+
+ if (rolledUpReactions.totalReactions === 0) return null;
+
+ return (
+
+
+
+
+ {rolledUpReactions.emojis.map((emoji, index) => (
+ {emoji}
+ ))}
+
+ {rolledUpReactions.totalReactions > 1 && (
+
+ {rolledUpReactions.totalReactions}
+
+ )}
+
+
+
+ );
+ },
+ (prevProps, nextProps) => {
+ if (prevProps.message.id !== nextProps.message.id) {
+ return false;
+ }
+ if (prevProps.message.lastUpdateAt !== nextProps.message.lastUpdateAt) {
+ return false;
+ }
+ // Compare reactions to ensure updates are not missed
+ if (prevProps.reactions !== nextProps.reactions) {
+ return false;
+ }
+ return true;
+ }
+);
+
+const useStyles = () => {
+ const { theme } = useAppTheme();
+
+ return StyleSheet.create({
+ reactionsWrapper: {
+ flexDirection: "row",
+ flexWrap: "wrap",
+ },
+ reactionButton: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingHorizontal: theme.spacing.xs,
+ paddingVertical: theme.spacing.xxs,
+ borderRadius: theme.borderRadius.sm,
+ borderWidth: theme.borderWidth.sm,
+ borderColor: theme.colors.border.subtle,
+ },
+ emojiContainer: {
+ flexDirection: "row",
+ flexWrap: "wrap",
+ gap: theme.spacing.xxxs,
+ },
+ reactorCount: {
+ marginLeft: theme.spacing.xxxs,
+ color: theme.colors.text.secondary,
+ },
+ });
+};
+
+const MAX_REACTION_EMOJIS_SHOWN = 3;
+
+function useMessageReactionsRolledUp(arg: {
+ reactions: MessageReactions;
+ userAddress: string;
+}) {
+ const { reactions, userAddress } = arg;
+
+ return useMemo((): RolledUpReactions => {
+ const details: { [content: string]: ReactionDetails } = {};
+ let totalReactions = 0;
+ let userReacted = false;
+
+ Object.values(reactions).forEach((reactionArray) => {
+ reactionArray.forEach((reaction) => {
+ if (!details[reaction.content]) {
+ details[reaction.content] = {
+ content: reaction.content,
+ count: 0,
+ userReacted: false,
+ reactors: [],
+ firstReactionTime: reaction.sent,
+ };
+ }
+ details[reaction.content].count++;
+ details[reaction.content].reactors.push(reaction.senderAddress);
+ if (
+ reaction.senderAddress.toLowerCase() === userAddress?.toLowerCase()
+ ) {
+ details[reaction.content].userReacted = true;
+ userReacted = true;
+ }
+ // Keep track of the earliest reaction time for this emoji
+ details[reaction.content].firstReactionTime = Math.min(
+ details[reaction.content].firstReactionTime,
+ reaction.sent
+ );
+ totalReactions++;
+ });
+ });
+
+ // Sort by the number of reactors in descending order
+ const sortedReactions = Object.values(details)
+ .sort((a, b) => b.reactors.length - a.reactors.length)
+ .slice(0, MAX_REACTION_EMOJIS_SHOWN)
+ .map((reaction) => reaction.content);
+
+ return { emojis: sortedReactions, totalReactions, userReacted, details };
+ }, [reactions, userAddress]);
+}
diff --git a/components/Chat/Message/MessageReactions/MessageReactions.types.ts b/components/Chat/Message/MessageReactions/MessageReactions.types.ts
new file mode 100644
index 000000000..e521a3d7f
--- /dev/null
+++ b/components/Chat/Message/MessageReactions/MessageReactions.types.ts
@@ -0,0 +1,26 @@
+import { MessageReaction } from "@utils/reactions";
+
+export type MessageReactions = {
+ [senderAddress: string]: MessageReaction[];
+};
+
+/**
+ * Aggregated reaction data including top emojis, total count, and detailed breakdown.
+ */
+export type RolledUpReactions = {
+ emojis: string[];
+ totalReactions: number;
+ userReacted: boolean;
+ details: Record;
+};
+
+/**
+ * Details for a specific reaction emoji, including count, reactors, and timing.
+ */
+export type ReactionDetails = {
+ content: string;
+ count: number;
+ userReacted: boolean;
+ reactors: string[];
+ firstReactionTime: number;
+};
diff --git a/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactions.store.tsx b/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactions.store.tsx
new file mode 100644
index 000000000..26367515d
--- /dev/null
+++ b/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactions.store.tsx
@@ -0,0 +1,31 @@
+import { create } from "zustand";
+
+import { RolledUpReactions } from "../MessageReactions.types";
+
+const initialMessageReactionsState: RolledUpReactions = {
+ emojis: [],
+ totalReactions: 0,
+ userReacted: false,
+ details: {},
+};
+
+export interface IMessageReactionsStore {
+ rolledUpReactions: RolledUpReactions;
+ setRolledUpReactions: (reactions: RolledUpReactions) => void;
+
+ // TODO: update state when new reactions come up and drawer is open
+ // updateReactions: (updates: Partial) => void;
+}
+
+export const useMessageReactionsStore = create(
+ (set) => ({
+ rolledUpReactions: initialMessageReactionsState,
+ setRolledUpReactions: (reactions) => set({ rolledUpReactions: reactions }),
+ })
+);
+
+export const resetMessageReactionsStore = () => {
+ useMessageReactionsStore
+ .getState()
+ .setRolledUpReactions(initialMessageReactionsState);
+};
diff --git a/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactionsDrawer.service.ts b/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactionsDrawer.service.ts
new file mode 100644
index 000000000..e3276f750
--- /dev/null
+++ b/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactionsDrawer.service.ts
@@ -0,0 +1,40 @@
+import { createBottomSheetModalRef } from "@design-system/BottomSheet/BottomSheet.utils";
+
+import { RolledUpReactions } from "../MessageReactions.types";
+import {
+ resetMessageReactionsStore,
+ useMessageReactionsStore,
+} from "./MessageReactions.store";
+
+export const bottomSheetModalRef = createBottomSheetModalRef();
+
+export function openMessageReactionsDrawer(
+ rolledUpReactions: RolledUpReactions
+) {
+ try {
+ if (!bottomSheetModalRef.current) {
+ throw new Error("Modal reference not initialized");
+ }
+ useMessageReactionsStore.getState().setRolledUpReactions(rolledUpReactions);
+ bottomSheetModalRef.current.present();
+ } catch (error) {
+ console.error("Failed to open message reactions drawer:", error);
+ resetMessageReactionsDrawer();
+ }
+}
+
+export function closeMessageReactionsDrawer(arg?: { resetStore?: boolean }) {
+ const { resetStore = true } = arg ?? {};
+ bottomSheetModalRef.current?.dismiss();
+ if (resetStore) {
+ resetMessageReactionsDrawer();
+ }
+}
+
+export function resetMessageReactionsDrawer() {
+ resetMessageReactionsStore();
+}
+
+export function useMessageReactionsRolledUpReactions() {
+ return useMessageReactionsStore((state) => state.rolledUpReactions);
+}
diff --git a/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactionsDrawer.tsx b/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactionsDrawer.tsx
new file mode 100644
index 000000000..32e54a4dc
--- /dev/null
+++ b/components/Chat/Message/MessageReactions/MessageReactionsDrawer/MessageReactionsDrawer.tsx
@@ -0,0 +1,79 @@
+import { BottomSheetContentContainer } from "@design-system/BottomSheet/BottomSheetContentContainer";
+import { BottomSheetHeader } from "@design-system/BottomSheet/BottomSheetHeader";
+import { BottomSheetModal } from "@design-system/BottomSheet/BottomSheetModal";
+import { HStack } from "@design-system/HStack";
+import { ScrollView } from "@design-system/ScrollView";
+import { Text } from "@design-system/Text";
+import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
+import { useAppTheme } from "@theme/useAppTheme";
+import { memo, useCallback } from "react";
+import { TouchableHighlight } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+import {
+ bottomSheetModalRef,
+ resetMessageReactionsDrawer,
+ useMessageReactionsRolledUpReactions,
+} from "./MessageReactionsDrawer.service";
+
+export const MessageReactionsDrawer = memo(function MessageReactionsDrawer() {
+ const { theme } = useAppTheme();
+
+ const insets = useSafeAreaInsets();
+
+ const rolledUpReactions = useMessageReactionsRolledUpReactions();
+
+ const handleDismiss = useCallback(() => {
+ resetMessageReactionsDrawer();
+ }, []);
+
+ return (
+
+
+
+
+
+ All {rolledUpReactions.totalReactions}
+ {Object.entries(rolledUpReactions.details).map(
+ ([emoji, details]) => (
+
+
+ {emoji}
+ {details.count}
+
+
+ )
+ )}
+
+
+
+
+
+ );
+});
diff --git a/design-system/BottomSheet/BottomSheetBackdropOpacity.tsx b/design-system/BottomSheet/BottomSheetBackdropOpacity.tsx
index 891b7d536..b79120584 100644
--- a/design-system/BottomSheet/BottomSheetBackdropOpacity.tsx
+++ b/design-system/BottomSheet/BottomSheetBackdropOpacity.tsx
@@ -4,15 +4,29 @@ import {
} from "@gorhom/bottom-sheet";
import { memo } from "react";
+import { useAppTheme } from "../../theme/useAppTheme";
+
export const BottomSheetBackdropOpacity = memo(function BackdropOpacity(
props: GorhomBottomSheetBackdropProps
) {
+ const { style, animatedIndex, animatedPosition, ...rest } = props;
+
+ const { theme } = useAppTheme();
+
return (
);
});
diff --git a/design-system/BottomSheet/BottomSheetModal.tsx b/design-system/BottomSheet/BottomSheetModal.tsx
index 34bcbf70c..c7488d161 100644
--- a/design-system/BottomSheet/BottomSheetModal.tsx
+++ b/design-system/BottomSheet/BottomSheetModal.tsx
@@ -10,6 +10,7 @@ import { FullWindowOverlay } from "react-native-screens";
import { BottomSheetBackdropOpacity } from "./BottomSheetBackdropOpacity";
import { BottomSheetHandleBar } from "./BottomSheetHandleBar";
+import { useAppTheme } from "../../theme/useAppTheme";
export type IBottomSheetModalProps = GorhomBottomSheetModalProps & {
absoluteHandleBar?: boolean;
@@ -18,7 +19,9 @@ export type IBottomSheetModalProps = GorhomBottomSheetModalProps & {
export const BottomSheetModal = memo(
forwardRef(
function BottomSheetModal(props, ref) {
- const { absoluteHandleBar = true, ...rest } = props;
+ const { absoluteHandleBar = true, backgroundStyle, ...rest } = props;
+
+ const { theme } = useAppTheme();
// https://github.com/gorhom/react-native-bottom-sheet/issues/1644#issuecomment-1949019839
const renderContainerComponent = useCallback((props: any) => {
@@ -40,8 +43,15 @@ export const BottomSheetModal = memo(
containerComponent={
Platform.OS === "ios" ? renderContainerComponent : undefined
}
+ enableDynamicSizing={false} // By default we don't want enable dynamic sizing
backdropComponent={BottomSheetBackdropOpacity}
handleComponent={renderHandleComponent}
+ backgroundStyle={[
+ {
+ backgroundColor: theme.colors.background.raised,
+ },
+ backgroundStyle,
+ ]}
{...rest}
/>
);