From 2065cd1db3a6a6767e657b76a8dce765ffd9ea27 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Thu, 12 Feb 2026 14:39:40 +0100 Subject: [PATCH 1/2] Adjust message reactions positioning, add offset, move replies to message inner --- src/components/Message/MessageSimple.tsx | 14 ++++---- src/components/Message/styling/Message.scss | 36 ++++++++++++++----- .../styling/MessageRepliesCountButton.scss | 4 +++ 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/components/Message/MessageSimple.tsx b/src/components/Message/MessageSimple.tsx index 301ddcd91..9c3d7433a 100644 --- a/src/components/Message/MessageSimple.tsx +++ b/src/components/Message/MessageSimple.tsx @@ -212,14 +212,14 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => { )} + {showReplyCountButton && ( + + )} + {showIsReplyInChannel && } - {showReplyCountButton && ( - - )} - {showIsReplyInChannel && } {showMetadata && (
diff --git a/src/components/Message/styling/Message.scss b/src/components/Message/styling/Message.scss index 5800315b7..c0181d4b7 100644 --- a/src/components/Message/styling/Message.scss +++ b/src/components/Message/styling/Message.scss @@ -75,6 +75,7 @@ --str-chat__message-reminder-border-inline-end: none; --str-chat__message-reminder-box-shadow: none; --str-chat__message-reminder-border-radius: 0; + --str-chat__message-reactions-host-offset-x: -6px; /* The maximum allowed width of the message component */ --str-chat__message-max-width: calc(var(--str-chat__spacing-px) * 480); @@ -210,7 +211,6 @@ grid-template-areas: '. message-reminder' 'avatar message' - 'avatar replies' 'avatar translation-notice' 'avatar custom-metadata' 'avatar metadata'; @@ -220,10 +220,18 @@ .str-chat__message-inner { .str-chat__message-reactions-host { - justify-content: flex-start; + justify-self: flex-start; &:has(.str-chat__message-reactions--flipped-horizontally) { - justify-content: flex-end; + justify-self: flex-end; + } + + &:has(.str-chat__message-reactions--top) { + margin-left: var(--str-chat__message-reactions-host-offset-x); + + &:has(.str-chat__message-reactions--flipped-horizontally) { + margin-right: var(--str-chat__message-reactions-host-offset-x); + } } } } @@ -233,7 +241,6 @@ grid-template-areas: 'message-reminder .' 'message avatar' - 'replies avatar' 'translation-notice avatar' 'custom-metadata avatar' 'metadata avatar'; @@ -243,10 +250,18 @@ .str-chat__message-inner { .str-chat__message-reactions-host { - justify-content: flex-end; + justify-self: flex-end; &:has(.str-chat__message-reactions--flipped-horizontally) { - justify-content: flex-start; + justify-self: flex-start; + } + + &:has(.str-chat__message-reactions--top) { + margin-right: var(--str-chat__message-reactions-host-offset-x); + + &:has(.str-chat__message-reactions--flipped-horizontally) { + margin-left: var(--str-chat__message-reactions-host-offset-x); + } } } } @@ -279,7 +294,8 @@ display: grid; grid-template-areas: 'reactions .' - 'message-bubble options'; + 'message-bubble options' + 'replies replies'; grid-template-columns: auto 1fr; column-gap: var(--str-chat__spacing-2); position: relative; @@ -287,7 +303,6 @@ .str-chat__message-reactions-host { display: flex; grid-area: reactions; - min-width: 100%; z-index: 1; &:has(.str-chat__message-reactions--top) { @@ -306,6 +321,7 @@ &:has(.str-chat__message-reactions--bottom) { grid-template-areas: 'message-bubble options' + 'replies replies' 'reactions .'; } @@ -338,7 +354,8 @@ grid-template-areas: 'reminder reminder' '. reactions' - 'options message-bubble'; + 'options message-bubble' + 'replies replies'; grid-template-columns: 1fr auto; .str-chat__message-options { @@ -349,6 +366,7 @@ grid-template-areas: 'reminder reminder' 'options message-bubble' + 'replies replies' '. reactions'; } } diff --git a/src/components/Message/styling/MessageRepliesCountButton.scss b/src/components/Message/styling/MessageRepliesCountButton.scss index 1339f7ce9..dfb398e22 100644 --- a/src/components/Message/styling/MessageRepliesCountButton.scss +++ b/src/components/Message/styling/MessageRepliesCountButton.scss @@ -50,6 +50,8 @@ // TODO: connector styling should be defined here but applied in Message.scss .str-chat__message.str-chat__message--me { .str-chat__message-replies-count-button-wrapper { + justify-self: flex-end; + .str-chat__message-replies-count-button { flex-direction: row; } @@ -70,6 +72,8 @@ .str-chat__message.str-chat__message--other { .str-chat__message-replies-count-button-wrapper { + justify-self: flex-start; + .str-chat__message-replies-count-button { flex-direction: row-reverse; } From 83ee067f6cad03fbb2079410a09d208592f2fd22 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Fri, 13 Feb 2026 18:22:00 +0100 Subject: [PATCH 2/2] Add thread_participants support, add reaction list button states, respect visual styles --- .../Message/MessageRepliesCountButton.tsx | 46 +++++----- src/components/Message/MessageSimple.tsx | 1 + src/components/Reactions/ReactionsList.tsx | 80 +++++++++++------ .../Reactions/styling/ReactionList.scss | 87 +++++++++++-------- 4 files changed, 131 insertions(+), 83 deletions(-) diff --git a/src/components/Message/MessageRepliesCountButton.tsx b/src/components/Message/MessageRepliesCountButton.tsx index 8fdcda56c..e1019d214 100644 --- a/src/components/Message/MessageRepliesCountButton.tsx +++ b/src/components/Message/MessageRepliesCountButton.tsx @@ -1,5 +1,7 @@ import type { MouseEventHandler } from 'react'; -import React from 'react'; +import type { UserResponse } from 'stream-chat'; +import React, { useMemo } from 'react'; + import { useTranslationContext } from '../../context/TranslationContext'; import { useChannelStateContext, useComponentContext } from '../../context'; import { AvatarStack as DefaultAvatarStack } from '../Avatar'; @@ -13,23 +15,39 @@ export type MessageRepliesCountButtonProps = { onClick?: MouseEventHandler; /* The amount of replies (i.e., threaded messages) on a message */ reply_count?: number; + thread_participants?: UserResponse[]; }; function UnMemoizedMessageRepliesCountButton(props: MessageRepliesCountButtonProps) { const { AvatarStack = DefaultAvatarStack } = useComponentContext( MessageRepliesCountButton.name, ); - const { labelPlural, labelSingle, onClick, reply_count = 0 } = props; + const { + labelPlural, + labelSingle, + onClick, + reply_count: replyCount = 0, + thread_participants: threadParticipants = [], + } = props; const { channelCapabilities } = useChannelStateContext(); const { t } = useTranslationContext('MessageRepliesCountButton'); - if (!reply_count) return null; + const avatarStackDisplayInfo = useMemo( + () => + threadParticipants.slice(0, 3).map((participant) => ({ + imageUrl: participant.image, + userName: participant.name || participant.id, + })), + [threadParticipants], + ); + + if (!replyCount) return null; - let replyCountText = t('replyCount', { count: reply_count }); + let replyCountText = t('replyCount', { count: replyCount }); - if (labelPlural && reply_count > 1) { - replyCountText = `${reply_count} ${labelPlural}`; + if (labelPlural && replyCount > 1) { + replyCountText = `${replyCount} ${labelPlural}`; } else if (labelSingle) { replyCountText = `1 ${labelSingle}`; } @@ -44,21 +62,7 @@ function UnMemoizedMessageRepliesCountButton(props: MessageRepliesCountButtonPro > {replyCountText} - +
); diff --git a/src/components/Message/MessageSimple.tsx b/src/components/Message/MessageSimple.tsx index 9c3d7433a..950c3cbeb 100644 --- a/src/components/Message/MessageSimple.tsx +++ b/src/components/Message/MessageSimple.tsx @@ -216,6 +216,7 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => { )} {showIsReplyInChannel && } diff --git a/src/components/Reactions/ReactionsList.tsx b/src/components/Reactions/ReactionsList.tsx index 9944e6d5f..54d401755 100644 --- a/src/components/Reactions/ReactionsList.tsx +++ b/src/components/Reactions/ReactionsList.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { type ComponentPropsWithoutRef, useState } from 'react'; import clsx from 'clsx'; import type { ReactionsListModalProps } from './ReactionsListModal'; @@ -53,6 +53,18 @@ export type ReactionsListProps = Partial< visualStyle?: 'clustered' | 'segmented' | null; }; +const FragmentOrButton = ({ + buttonIf: renderButton = false, + children, + ...props +}: ComponentPropsWithoutRef<'button'> & { buttonIf?: boolean }) => { + if (renderButton) { + return ; + } + + return <>{children}; +}; + const UnMemoizedReactionsList = (props: ReactionsListProps) => { const { flipHorizontalPosition = false, @@ -96,36 +108,48 @@ const UnMemoizedReactionsList = (props: ReactionsListProps) => { })} role='figure' > -
    - {existingReactions.map( - ({ EmojiComponent, reactionCount, reactionType }) => - EmojiComponent && ( -
  • - -
  • - ), + {visualStyle === 'segmented' && ( + + {reactionCount} + + )} + + + ), + )} +
+ {visualStyle === 'clustered' && ( + + {totalReactionCount} + )} - - {visualStyle === 'clustered' && ( - - {totalReactionCount} - - )} + {selectedReactionType !== null && (