From c05a12cf89786e3fa509af1c47d42d86784df7fd Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Fri, 10 Apr 2026 12:50:20 -0700 Subject: [PATCH] Add dialog for viewing clip clop reactions (#10228) --- src/components/ContextMenu/index.tsx | 7 +- src/components/ContextMenu/types.ts | 1 + src/components/dms/MessageContextMenu.tsx | 10 +- src/components/dms/MessageItem.tsx | 382 +++++++++++++++--- .../Messages/components/MessagesList.tsx | 4 +- 5 files changed, 336 insertions(+), 68 deletions(-) diff --git a/src/components/ContextMenu/index.tsx b/src/components/ContextMenu/index.tsx index e94eaf7795..cce4332dc4 100644 --- a/src/components/ContextMenu/index.tsx +++ b/src/components/ContextMenu/index.tsx @@ -482,7 +482,11 @@ function TriggerClone({ ) } -export function AuxiliaryView({children, align = 'left'}: AuxiliaryViewProps) { +export function AuxiliaryView({ + children, + align = 'left', + style, +}: AuxiliaryViewProps) { const context = useContextMenuContext() const {width: screenWidth} = useWindowDimensions() const {top: topInset} = useSafeAreaInsets() @@ -556,6 +560,7 @@ export function AuxiliaryView({children, align = 'left'}: AuxiliaryViewProps) { : {right: screenWidth - measurement.x - measurement.width}, animatedStyle, a.z_20, + style, ]}> {children} diff --git a/src/components/ContextMenu/types.ts b/src/components/ContextMenu/types.ts index 7d4f3019a8..260d95e85c 100644 --- a/src/components/ContextMenu/types.ts +++ b/src/components/ContextMenu/types.ts @@ -21,6 +21,7 @@ export type { export type AuxiliaryViewProps = { children?: React.ReactNode align?: 'left' | 'right' + style?: StyleProp } export type ItemProps = Omit & { diff --git a/src/components/dms/MessageContextMenu.tsx b/src/components/dms/MessageContextMenu.tsx index 5cae1adfc8..2460aa585d 100644 --- a/src/components/dms/MessageContextMenu.tsx +++ b/src/components/dms/MessageContextMenu.tsx @@ -11,6 +11,7 @@ import {useConvoActive} from '#/state/messages/convo' import {useLanguagePrefs} from '#/state/preferences' import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' import {useSession} from '#/state/session' +import {atoms as a} from '#/alf' import * as ContextMenu from '#/components/ContextMenu' import {type TriggerProps} from '#/components/ContextMenu/types' import {AfterReportDialog} from '#/components/dms/AfterReportDialog' @@ -46,6 +47,7 @@ export let MessageContextMenu = ({ const translate = useGoogleTranslate() const isFromSelf = message.sender?.did === currentAccount?.did + const isGroupChatEnabled = ax.features.enabled(ax.features.GroupChatsEnable) const onCopyMessage = useCallback(() => { const str = richTextToString( @@ -114,7 +116,9 @@ export let MessageContextMenu = ({ <> {IS_NATIVE && ( - + - + {message.text.length > 0 && ( <> () - for (const react of reactions) { - if (!react) continue - const existing = grouped.get(react.value) + for (const reaction of reactions) { + if (!reaction) continue + const existing = grouped.get(reaction.value) if (existing) { - existing.senders.push(react.sender) + existing.senders.push(reaction.sender) existing.count++ } else { - grouped.set(react.value, { - value: react.value, - senders: [react.sender], + grouped.set(reaction.value, { + key: reaction.value, + value: reaction.value, + senders: [reaction.sender], count: 1, }) } @@ -227,62 +248,74 @@ let MessageItem = ({ const appliedReactions = ( {hasReactions ? ( - + <> - {groupedReactions.map(group => ( - 1 && native(ZoomOut.delay(200)) - } - layout={native(LinearTransition.delay(300))} - key={group.value} - style={[a.p_2xs]}> - - {group.value} - - - ))} - {groupedReactions.length !== reactions.length && - reactions.length > 1 ? ( - - - {reactions.length} - - - ) : null} + + isGroupChat ? reactionsControl.open() : undefined + }> + {groupedReactions.map(group => ( + 1 && native(ZoomOut.delay(200)) + } + layout={native(LinearTransition.delay(300))} + key={group.value} + style={[a.p_2xs]}> + + {group.value} + + + ))} + {groupedReactions.length !== reactions.length && + reactions.length > 1 ? ( + + + {reactions.length} + + + ) : null} + - + + ) : null} ) @@ -301,7 +334,9 @@ let MessageItem = ({ ]}> {isGroupChat && !isFromSelf && isLastInCluster ? ( - {avatar} + + {avatar} + ) : null} {rt.text.length > 0 && ( )} + {appliedReactions} - {appliedReactions} {isLastInCluster && ( { + setSelected(value) + } + + const filteredMembers = + selected === 'all' + ? members + : members.filter(m => + reactions?.some(r => r.sender.did === m.did && r.value === selected), + ) + + const minHeight = members.length * ROW_HEIGHT + + return ( + setSelected('all')} + nativeOptions={{preventExpansion: true, minHeight}}> + + + + Reactions + + + + + {filteredMembers.map(profile => { + const displayName = sanitizeDisplayName( + profile?.displayName || sanitizeHandle(profile?.handle ?? ''), + ) + const handle = sanitizeHandle(profile?.handle ?? '', '@') + const reaction = reactions?.find( + ({sender}) => sender.did === profile.did, + ) + const rt = reaction + ? new RichTextAPI({text: reaction.value}) + : undefined + + return rt ? ( + + + + + + {displayName} + + + {handle} + + + + + + + + ) : null + })} + + + ) +} + +function ReactionTabs({ + groupedReactions, + selected, + totalReactions, + onFilter, +}: { + groupedReactions?: Reaction[] + selected: string + totalReactions: number + onFilter: (value: string) => void +}) { + const t = useTheme() + const {t: l} = useLingui() + + const contentSize = useSharedValue(0) + const scrollX = useSharedValue(0) + + const handlePress = (value: string) => { + onFilter(value) + } + + const tabs = [ + { + key: 'all', + value: l`All`, + senders: [], + count: totalReactions, + } as Reaction, + ...(groupedReactions ?? []), + ] + + return ( + + { + scrollX.set(Math.round(e.nativeEvent.contentOffset.x)) + }}> + { + contentSize.set(e.nativeEvent.layout.width) + }}> + {tabs?.map((reaction, index) => ( + + ))} + + + + ) +} + +function ReactionTab({ + index, + reaction, + selected, + total, + onPress, +}: { + index: number + reaction: Reaction + selected: string + total: number + onPress: (value: string) => void +}) { + const t = useTheme() + const {t: l} = useLingui() + + return ( + onPress(reaction.key)}> + + {l`${reaction.value} ${reaction.count}`} + + + ) +} diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index 617582d824..985bf242bd 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -471,7 +471,9 @@ export function MessagesList({ {ax.features.enabled(ax.features.DmsNewMessageComposerEnable) ? ( + void onSendMessage(message) + } hasEmbed={!!embedUri} setEmbed={setEmbed}>