From 72df08e0401511de83b275570031752c7b818042 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Thu, 9 Apr 2026 00:51:15 +0200 Subject: [PATCH 1/6] Initial commit --- src/components/Dialog/hooks/useDialog.ts | 11 ++- .../Dialog/service/DialogManager.ts | 47 ++++++--- src/components/Icons/icons.tsx | 26 +++++ src/components/Message/styling/Message.scss | 8 +- .../Reactions/MessageReactionsDetail.tsx | 50 ++++++++-- src/components/Reactions/ReactionSelector.tsx | 97 +++++++++++++------ .../styling/MessageReactionsDetail.scss | 25 ++++- .../Reactions/styling/ReactionSelector.scss | 23 ----- .../styling/ReactionSelectorExtendedList.scss | 22 +++++ src/components/Reactions/styling/index.scss | 1 + 10 files changed, 220 insertions(+), 90 deletions(-) create mode 100644 src/components/Reactions/styling/ReactionSelectorExtendedList.scss diff --git a/src/components/Dialog/hooks/useDialog.ts b/src/components/Dialog/hooks/useDialog.ts index e81ec1ae5..75a713af4 100644 --- a/src/components/Dialog/hooks/useDialog.ts +++ b/src/components/Dialog/hooks/useDialog.ts @@ -22,16 +22,17 @@ export const useDialog = ({ }: UseDialogParams) => { const { dialogManager } = useDialogManager({ dialogManagerId }); - useEffect( - () => () => { + useEffect(() => { + dialogManager.cancelPendingRemoval(id); + + return () => { // Since this cleanup can run even if the component is still mounted // and dialog id is unchanged (e.g. in ), it's safer to // mark state as unused and only remove it after a timeout, rather than // to remove it immediately. dialogManager.markForRemoval(id); - }, - [dialogManager, id], - ); + }; + }, [dialogManager, id]); return dialogManager.getOrCreate({ closeOnClickOutside, id }); }; diff --git a/src/components/Dialog/service/DialogManager.ts b/src/components/Dialog/service/DialogManager.ts index 6fb2470e2..1f11a826c 100644 --- a/src/components/Dialog/service/DialogManager.ts +++ b/src/components/Dialog/service/DialogManager.ts @@ -70,12 +70,12 @@ export class DialogManager { ); } - get(id: DialogId) { + get(id: DialogId): Dialog | undefined { return this.state.getLatestValue().dialogsById[id]; } getOrCreate({ closeOnClickOutside, id }: GetOrCreateDialogParams) { - let dialog = this.state.getLatestValue().dialogsById[id]; + let dialog = this.get(id); if (!dialog) { dialog = { close: () => { @@ -97,7 +97,7 @@ export class DialogManager { }; this.state.next((current) => ({ ...current, - dialogsById: { ...current.dialogsById, [id]: dialog }, + dialogsById: { ...current.dialogsById, [id]: dialog as Dialog }, })); } @@ -106,16 +106,15 @@ export class DialogManager { if (shouldUpdateDialogSettings) { if (dialog.removalTimeout) clearTimeout(dialog.removalTimeout); - dialog = { - ...dialog, - closeOnClickOutside, - removalTimeout: undefined, - }; this.state.next((current) => ({ ...current, dialogsById: { ...current.dialogsById, - [id]: dialog, + [id]: { + ...current.dialogsById[id], + closeOnClickOutside, + removalTimeout: undefined, + }, }, })); } @@ -158,9 +157,8 @@ export class DialogManager { } } - remove(id: DialogId) { - const state = this.state.getLatestValue(); - const dialog = state.dialogsById[id]; + remove = (id: DialogId) => { + const dialog = this.get(id); if (!dialog) return; if (dialog.removalTimeout) { @@ -175,7 +173,7 @@ export class DialogManager { dialogsById: newDialogs, }; }); - } + }; /** * Marks the dialog state as unused. If the dialog id is referenced again quickly, @@ -183,7 +181,7 @@ export class DialogManager { * a short timeout. */ markForRemoval(id: DialogId) { - const dialog = this.state.getLatestValue().dialogsById[id]; + const dialog = this.get(id); if (!dialog) { return; @@ -202,4 +200,25 @@ export class DialogManager { }, })); } + + cancelPendingRemoval(id: DialogId) { + const dialog = this.get(id); + + if (!dialog?.removalTimeout) { + return; + } + + clearTimeout(dialog.removalTimeout); + + this.state.next((current) => ({ + ...current, + dialogsById: { + ...current.dialogsById, + [id]: { + ...current.dialogsById[id], + removalTimeout: undefined, + }, + }, + })); + } } diff --git a/src/components/Icons/icons.tsx b/src/components/Icons/icons.tsx index 1722cd406..4acbac479 100644 --- a/src/components/Icons/icons.tsx +++ b/src/components/Icons/icons.tsx @@ -495,6 +495,32 @@ export const IconEmoji = createIcon( , ); +export const IconEmojiAdd = createIcon( + 'IconEmojiAdd', + <> + + + + + + , +); + // was: IconExclamation export const IconExclamationMarkFill = createIcon( 'IconExclamationMarkFill', diff --git a/src/components/Message/styling/Message.scss b/src/components/Message/styling/Message.scss index 07d94fb22..06d23b456 100644 --- a/src/components/Message/styling/Message.scss +++ b/src/components/Message/styling/Message.scss @@ -278,9 +278,7 @@ var(--str-chat__message-reactions-host-offset-x) * -1 ); - &:has(.str-chat__message-reactions--flipped-horizontally) { - margin-inline-end: var(--str-chat__message-reactions-host-offset-x); - } + margin-inline-end: var(--str-chat__message-reactions-host-offset-x); } .str-chat__message-reactions.str-chat__message-reactions--segmented.str-chat__message-reactions--bottom @@ -323,9 +321,7 @@ &:has(.str-chat__message-reactions--top) { padding-inline-end: calc(var(--str-chat__message-reactions-host-offset-x) * -1); - &:has(.str-chat__message-reactions--flipped-horizontally) { - margin-inline-start: var(--str-chat__message-reactions-host-offset-x); - } + margin-inline-start: var(--str-chat__message-reactions-host-offset-x); } .str-chat__message-reactions.str-chat__message-reactions--segmented.str-chat__message-reactions--bottom diff --git a/src/components/Reactions/MessageReactionsDetail.tsx b/src/components/Reactions/MessageReactionsDetail.tsx index b4afddee2..22e6a3367 100644 --- a/src/components/Reactions/MessageReactionsDetail.tsx +++ b/src/components/Reactions/MessageReactionsDetail.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import type { ReactionSummary, ReactionType } from './types'; @@ -14,6 +14,8 @@ import { import type { ReactionSort } from 'stream-chat'; import { defaultReactionOptions } from './reactionOptions'; import type { useProcessReactions } from './hooks/useProcessReactions'; +import { IconEmojiAdd } from '../Icons'; +import { ReactionSelector, type ReactionSelectorProps } from './ReactionSelector'; export type MessageReactionsDetailProps = Partial< Pick @@ -24,7 +26,7 @@ export type MessageReactionsDetailProps = Partial< sort?: ReactionSort; totalReactionCount?: number; reactionGroups?: ReturnType['reactionGroups']; -}; +} & ReactionSelectorProps; const defaultReactionDetailsSort = { created_at: -1 } as const; @@ -45,13 +47,16 @@ export const MessageReactionsDetailLoadingIndicator = () => { export function MessageReactionsDetail({ handleFetchReactions, + handleReaction, onSelectedReactionTypeChange, + own_reactions, reactionDetailsSort: propReactionDetailsSort, reactionGroups, reactions, selectedReactionType, totalReactionCount, }: MessageReactionsDetailProps) { + const [extendedReactionListOpen, setExtendedReactionListOpen] = useState(false); const { client } = useChatContext(); const { Avatar = DefaultAvatar, @@ -62,6 +67,7 @@ export function MessageReactionsDetail({ const { handleReaction: contextHandleReaction, + message, reactionDetailsSort: contextReactionDetailsSort, } = useMessageContext(MessageReactionsDetail.name); @@ -79,6 +85,21 @@ export function MessageReactionsDetail({ sort: reactionDetailsSort, }); + if (extendedReactionListOpen) { + return ( +
+ +
+ ); + } + return (
    +
  • + +
  • + {reactions.map( ({ EmojiComponent, reactionCount, reactionType }) => EmojiComponent && ( @@ -110,14 +142,12 @@ export function MessageReactionsDetail({ - {reactionCount > 1 && ( - - {reactionCount} - - )} + + {reactionCount} + ), diff --git a/src/components/Reactions/ReactionSelector.tsx b/src/components/Reactions/ReactionSelector.tsx index 9679fa0e8..ca16cd0e8 100644 --- a/src/components/Reactions/ReactionSelector.tsx +++ b/src/components/Reactions/ReactionSelector.tsx @@ -1,7 +1,7 @@ import React, { type ReactNode, useMemo, useState } from 'react'; import clsx from 'clsx'; -import { useDialog } from '../Dialog'; +import { useDialogOnNearestManager } from '../Dialog'; import { defaultReactionOptions } from './reactionOptions'; import { useComponentContext } from '../../context/ComponentContext'; import { useMessageContext } from '../../context/MessageContext'; @@ -24,6 +24,7 @@ interface ReactionSelectorInterface { (props: ReactionSelectorProps): ReactNode; getDialogId: (_: { messageId: string; threadList?: boolean }) => string; displayName: string; + ExtendedList: React.ComponentType; } const stableOwnReactions: ReactionResponse[] = []; @@ -45,7 +46,7 @@ export const ReactionSelector: ReactionSelectorInterface = (props) => { messageId: message.id, threadList, }); - const dialog = useDialog({ id: dialogId }); + const { dialog } = useDialogOnNearestManager({ id: dialogId }); const handleReaction = propHandleReaction ?? contextHandleReaction; const ownReactions = propOwnReactions ?? message?.own_reactions ?? stableOwnReactions; @@ -114,34 +115,9 @@ export const ReactionSelector: ReactionSelectorInterface = (props) => { )} - {extendedListOpen && - !Array.isArray(reactionOptions) && - reactionOptions.extended && ( -
    - {Object.entries(reactionOptions.extended).map( - ([reactionType, { Component, name: reactionName }]) => ( - - ), - )} -
    - )} + {extendedListOpen && ( + + )}
); }; @@ -152,3 +128,64 @@ ReactionSelector.getDialogId = ({ messageId, threadList }) => { }; ReactionSelector.displayName = 'ReactionSelector'; + +ReactionSelector.ExtendedList = function ReactionSelectorExtendedList({ + dialogId, + handleReaction: propHandleReaction, + own_reactions: propOwnReactions, +}) { + const { reactionOptions = defaultReactionOptions } = useComponentContext( + 'ReactionSelector.ExtendedList', + ); + const { + closeReactionSelectorOnClick, + handleReaction: contextHandleReaction, + message, + } = useMessageContext('ReactionSelector'); + + const handleReaction = propHandleReaction ?? contextHandleReaction; + const ownReactions = propOwnReactions ?? message?.own_reactions ?? stableOwnReactions; + + const { dialog } = useDialogOnNearestManager({ id: dialogId || '' }); + + const ownReactionByType = useMemo(() => { + const map: { [key: string]: ReactionResponse } = {}; + + for (const reaction of ownReactions) { + map[reaction.type] ??= reaction; + } + + return map; + }, [ownReactions]); + + if (Array.isArray(reactionOptions) || !reactionOptions.extended) { + return null; + } + + return ( +
+ {Object.entries(reactionOptions.extended).map( + ([reactionType, { Component, name: reactionName }]) => ( + + ), + )} +
+ ); +}; diff --git a/src/components/Reactions/styling/MessageReactionsDetail.scss b/src/components/Reactions/styling/MessageReactionsDetail.scss index 809dcdd07..d567c3dc8 100644 --- a/src/components/Reactions/styling/MessageReactionsDetail.scss +++ b/src/components/Reactions/styling/MessageReactionsDetail.scss @@ -11,8 +11,17 @@ .str-chat__message-reactions-detail { border-radius: var(--radius-lg); background: var(--background-core-elevation-2); - max-width: 256px; - min-width: min(90vw, 256px); + max-width: 278px; + min-width: min(90vw, 278px); + + &:has(.str-chat__reaction-selector-extended-list) { + padding: 0; + display: block; + @include utils.scrollable-y; + scrollbar-width: none; + border-radius: var(--radius-lg); + max-height: 250px; + } &::after { content: ''; @@ -68,12 +77,24 @@ .str-chat__message-reactions-detail__reaction-type-list-item-button { @include common.reaction-button; box-shadow: unset; + min-width: var(--size-48); .str-chat__message-reactions-detail__reaction-type-list-item-icon { font-family: system-ui; font-size: var(--font-size-size-17); font-style: normal; line-height: var(--typography-line-height-normal); + + .str-chat__icon { + width: var(--icon-size-sm); + height: var(--icon-size-sm); + } + + &:has(.str-chat__icon) { + display: flex; + align-items: center; + justify-content: center; + } } .str-chat__message-reactions-detail__reaction-type-list-item-count { diff --git a/src/components/Reactions/styling/ReactionSelector.scss b/src/components/Reactions/styling/ReactionSelector.scss index 499764caa..40bf42f99 100644 --- a/src/components/Reactions/styling/ReactionSelector.scss +++ b/src/components/Reactions/styling/ReactionSelector.scss @@ -33,29 +33,6 @@ } } - .str-chat__reaction-selector-extended-list { - display: grid; - grid-template-columns: repeat(7, 1fr); - height: 100%; - padding-block: var(--spacing-md); - padding-inline: var(--spacing-sm); - - .str-chat__reaction-selector-extended-list__button { - .str-chat__reaction-icon { - height: var(--emoji-md); - width: var(--emoji-md); - font-size: var(--emoji-md); - letter-spacing: var(--typography-letter-spacing-default); - font-style: normal; - line-height: 0; - font-family: system-ui; - display: flex; - justify-content: center; - align-items: center; - } - } - } - .str-chat__reaction-selector-list { list-style: none; margin: var(--spacing-none, 0); diff --git a/src/components/Reactions/styling/ReactionSelectorExtendedList.scss b/src/components/Reactions/styling/ReactionSelectorExtendedList.scss new file mode 100644 index 000000000..fb0ca14a5 --- /dev/null +++ b/src/components/Reactions/styling/ReactionSelectorExtendedList.scss @@ -0,0 +1,22 @@ +.str-chat__reaction-selector-extended-list { + display: grid; + grid-template-columns: repeat(7, 1fr); + height: 100%; + padding-block: var(--spacing-md); + padding-inline: var(--spacing-sm); + + .str-chat__reaction-selector-extended-list__button { + .str-chat__reaction-icon { + height: var(--emoji-md); + width: var(--emoji-md); + font-size: var(--emoji-md); + letter-spacing: var(--typography-letter-spacing-default); + font-style: normal; + line-height: 0; + font-family: system-ui; + display: flex; + justify-content: center; + align-items: center; + } + } +} diff --git a/src/components/Reactions/styling/index.scss b/src/components/Reactions/styling/index.scss index 364ea1d65..4490d7d54 100644 --- a/src/components/Reactions/styling/index.scss +++ b/src/components/Reactions/styling/index.scss @@ -1,3 +1,4 @@ @use 'ReactionSelector'; +@use 'ReactionSelectorExtendedList'; @use 'MessageReactions'; @use 'MessageReactionsDetail'; From c598a702e196a4db132c52896621bf707a703d0e Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Thu, 9 Apr 2026 12:59:36 +0200 Subject: [PATCH 2/6] Finalize --- src/components/Reactions/MessageReactions.tsx | 7 ++++-- .../Reactions/MessageReactionsDetail.tsx | 22 ++++++++++++++----- src/components/Reactions/ReactionSelector.tsx | 8 ++++--- src/context/ComponentContext.tsx | 7 +++++- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/components/Reactions/MessageReactions.tsx b/src/components/Reactions/MessageReactions.tsx index 036444422..7ee0b0ec6 100644 --- a/src/components/Reactions/MessageReactions.tsx +++ b/src/components/Reactions/MessageReactions.tsx @@ -21,6 +21,7 @@ import { MAX_MESSAGE_REACTIONS_TO_FETCH } from '../Message/hooks'; import type { ReactionGroupResponse, ReactionResponse } from 'stream-chat'; import type { ReactionsComparator, ReactionType } from './types'; import { DialogAnchor, useDialogIsOpen, useDialogOnNearestManager } from '../Dialog'; +import { ReactionSelector } from './ReactionSelector'; export type MessageReactionsProps = Partial< Pick @@ -66,7 +67,6 @@ const UnMemoizedMessageReactions = (props: MessageReactionsProps) => { capLimit: { clustered: capLimitClustered = 5, segmented: capLimitSegmented = 4 } = {}, flipHorizontalPosition = false, handleFetchReactions, - // eslint-disable-next-line @typescript-eslint/no-unused-vars reactionDetailsSort, verticalPosition = 'top', visualStyle = 'clustered', @@ -89,7 +89,9 @@ const UnMemoizedMessageReactions = (props: MessageReactionsProps) => { const { isMyMessage, message } = useMessageContext('MessageReactions'); const divRef = useRef>(null); - const dialogId = `message-reactions-detail-${message.id}`; + const dialogId = DefaultMessageReactionsDetail.getDialogId({ + messageId: message.id, + }); const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId }); const isDialogOpen = useDialogIsOpen(dialogId, dialogManager?.id); @@ -225,6 +227,7 @@ const UnMemoizedMessageReactions = (props: MessageReactionsProps) => { { return <>{elements}; }; -export function MessageReactionsDetail({ +interface MessageReactionsDetailInterface { + (props: MessageReactionsDetailProps): React.ReactNode; + displayName: string; + getDialogId: (_: { messageId: string }) => string; +} + +export const MessageReactionsDetail: MessageReactionsDetailInterface = ({ handleFetchReactions, handleReaction, onSelectedReactionTypeChange, @@ -55,13 +61,14 @@ export function MessageReactionsDetail({ reactions, selectedReactionType, totalReactionCount, -}: MessageReactionsDetailProps) { +}) => { const [extendedReactionListOpen, setExtendedReactionListOpen] = useState(false); const { client } = useChatContext(); const { Avatar = DefaultAvatar, LoadingIndicator = MessageReactionsDetailLoadingIndicator, reactionOptions = defaultReactionOptions, + ReactionSelectorExtendedList = ReactionSelector.ExtendedList, } = useComponentContext(MessageReactionsDetail.name); const { t } = useTranslationContext(); @@ -91,8 +98,8 @@ export function MessageReactionsDetail({ className='str-chat__message-reactions-detail' data-testid='message-reactions-detail' > - @@ -220,4 +227,9 @@ export function MessageReactionsDetail({ ); -} +}; + +MessageReactionsDetail.displayName = 'MessageReactionsDetail'; + +MessageReactionsDetail.getDialogId = ({ messageId }) => + `message-reactions-detail-${messageId}`; diff --git a/src/components/Reactions/ReactionSelector.tsx b/src/components/Reactions/ReactionSelector.tsx index ca16cd0e8..b96f3eb1f 100644 --- a/src/components/Reactions/ReactionSelector.tsx +++ b/src/components/Reactions/ReactionSelector.tsx @@ -33,8 +33,10 @@ export const ReactionSelector: ReactionSelectorInterface = (props) => { const { handleReaction: propHandleReaction, own_reactions: propOwnReactions } = props; const [extendedListOpen, setExtendedListOpen] = useState(false); - const { reactionOptions = defaultReactionOptions } = - useComponentContext('ReactionSelector'); + const { + reactionOptions = defaultReactionOptions, + ReactionSelectorExtendedList = ReactionSelector.ExtendedList, + } = useComponentContext('ReactionSelector'); const { closeReactionSelectorOnClick, @@ -116,7 +118,7 @@ export const ReactionSelector: ReactionSelectorInterface = (props) => { )} {extendedListOpen && ( - + )} ); diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index 8effb1042..b9440a8d8 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -1,6 +1,7 @@ -import type { PropsWithChildren } from 'react'; +import type { ComponentProps, PropsWithChildren } from 'react'; import React, { useContext } from 'react'; + import { type AttachmentPreviewListProps, type AttachmentProps, @@ -43,6 +44,7 @@ import { type PollOptionSelectorProps, type QuotedMessagePreviewProps, type ReactionOptions, + type ReactionSelector, type ReactionSelectorProps, type RecordingPermissionDeniedNotificationProps, type ReminderNotificationProps, @@ -211,6 +213,9 @@ export type ComponentContextValue = { reactionOptions?: ReactionOptions; /** Custom UI component to display the reaction selector, defaults to and accepts same props as: [ReactionSelector](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Reactions/ReactionSelector.tsx) */ ReactionSelector?: React.ForwardRefExoticComponent; + ReactionSelectorExtendedList?: React.ComponentType< + ComponentProps<(typeof ReactionSelector)['ExtendedList']> + >; /** Custom UI component to display the list of reactions on a message, defaults to and accepts same props as: [MessageReactions](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Reactions/MessageReactions.tsx) */ MessageReactions?: React.ComponentType; /** Custom UI component to display the reactions modal, defaults to and accepts same props as: [MessageReactionsDetail](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Reactions/MessageReactionsDetail.tsx) */ From cffb7d566d0466f009c4e2e3551175244fa4a129 Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Thu, 9 Apr 2026 14:44:25 +0200 Subject: [PATCH 3/6] Add tests --- src/components/Reactions/MessageReactions.tsx | 13 +- .../Reactions/MessageReactionsDetail.tsx | 9 +- src/components/Reactions/ReactionSelector.tsx | 11 +- .../__tests__/MessageReactions.test.tsx | 95 ++++--- .../__tests__/MessageReactionsDetail.test.tsx | 201 ++++++++++++--- .../__tests__/ReactionSelector.test.tsx | 237 +++++++++++++++--- src/context/ComponentContext.tsx | 1 - 7 files changed, 453 insertions(+), 114 deletions(-) diff --git a/src/components/Reactions/MessageReactions.tsx b/src/components/Reactions/MessageReactions.tsx index 7ee0b0ec6..165646e1c 100644 --- a/src/components/Reactions/MessageReactions.tsx +++ b/src/components/Reactions/MessageReactions.tsx @@ -21,7 +21,6 @@ import { MAX_MESSAGE_REACTIONS_TO_FETCH } from '../Message/hooks'; import type { ReactionGroupResponse, ReactionResponse } from 'stream-chat'; import type { ReactionsComparator, ReactionType } from './types'; import { DialogAnchor, useDialogIsOpen, useDialogOnNearestManager } from '../Dialog'; -import { ReactionSelector } from './ReactionSelector'; export type MessageReactionsProps = Partial< Pick @@ -160,6 +159,7 @@ const UnMemoizedMessageReactions = (props: MessageReactionsProps) => { aria-pressed={isDialogOpen} buttonIf={visualStyle === 'clustered'} className='str-chat__message-reactions__list-button' + data-testid='message-reactions-list-button' onClick={() => handleReactionButtonClick(null)} >
    @@ -168,6 +168,7 @@ const UnMemoizedMessageReactions = (props: MessageReactionsProps) => { EmojiComponent && (
  • { className='str-chat__message-reactions__list-item-button' onClick={() => handleReactionButtonClick(reactionType)} > - + {visualStyle === 'segmented' && reactionCount > 1 && ( @@ -208,7 +212,10 @@ const UnMemoizedMessageReactions = (props: MessageReactionsProps) => { )}
{visualStyle === 'clustered' && ( - + {totalReactionCount} )} diff --git a/src/components/Reactions/MessageReactionsDetail.tsx b/src/components/Reactions/MessageReactionsDetail.tsx index 7cdeb66d3..3c6dca72a 100644 --- a/src/components/Reactions/MessageReactionsDetail.tsx +++ b/src/components/Reactions/MessageReactionsDetail.tsx @@ -118,10 +118,15 @@ export const MessageReactionsDetail: MessageReactionsDetailInterface = ({ )}
-
    +
- {EmojiComponent && !selectedReactionType && } + {!selectedReactionType && EmojiComponent && } ); diff --git a/src/components/Reactions/styling/MessageReactionsDetail.scss b/src/components/Reactions/styling/MessageReactionsDetail.scss index d567c3dc8..37c297df1 100644 --- a/src/components/Reactions/styling/MessageReactionsDetail.scss +++ b/src/components/Reactions/styling/MessageReactionsDetail.scss @@ -29,10 +29,14 @@ width: 100%; bottom: 0; inset-inline-start: 0; - height: var(--size-12); + height: var(--size-16); border-end-end-radius: inherit; border-end-start-radius: inherit; - background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 5%, rgba(0, 0, 0, 0.1) 130%); + background: linear-gradient( + to bottom, + transparent 5%, + var(--background-core-elevation-0) 95% + ); } font-family: var(--typography-font-family-sans); @@ -65,7 +69,7 @@ .str-chat__message-reactions-detail__reaction-type-list { list-style: none; margin: 0; - padding-inline: var(--spacing-xs); + padding-inline: var(--spacing-md); padding-block: var(--spacing-xs); display: flex; flex-wrap: wrap; From 9ad6b561ac3ff06b4d62aff42a5b61830d90254a Mon Sep 17 00:00:00 2001 From: Anton Arnautov Date: Thu, 9 Apr 2026 20:23:51 +0200 Subject: [PATCH 6/6] Post-review adjustments vol.2 --- .../Reactions/MessageReactionsDetail.tsx | 130 +++++++++--------- .../styling/MessageReactionsDetail.scss | 54 +++----- .../Reactions/styling/ReactionSelector.scss | 10 +- src/components/Reactions/styling/common.scss | 35 +++++ 4 files changed, 127 insertions(+), 102 deletions(-) diff --git a/src/components/Reactions/MessageReactionsDetail.tsx b/src/components/Reactions/MessageReactionsDetail.tsx index b241e70d3..d2fc8d6d7 100644 --- a/src/components/Reactions/MessageReactionsDetail.tsx +++ b/src/components/Reactions/MessageReactionsDetail.tsx @@ -166,72 +166,74 @@ export const MessageReactionsDetail: MessageReactionsDetailInterface = ({ )} -
- {areReactionsLoading && } - {!areReactionsLoading && ( - <> - {reactionDetails.map(({ type, user }) => { - const belongsToCurrentUser = client.user?.id === user?.id; - const EmojiComponent = Array.isArray(reactionOptions) - ? undefined - : (reactionOptions.quick[type]?.Component ?? - reactionOptions.extended?.[type]?.Component); - - return ( -
- -
- - {belongsToCurrentUser ? t('You') : user?.name || user?.id} - - {belongsToCurrentUser && ( - - )} + {belongsToCurrentUser ? t('You') : user?.name || user?.id} + + {belongsToCurrentUser && ( + + )} +
+ + {!selectedReactionType && EmojiComponent && } +
- - {!selectedReactionType && EmojiComponent && } - -
- ); - })} - - )} + ); + })} + + )} + ); diff --git a/src/components/Reactions/styling/MessageReactionsDetail.scss b/src/components/Reactions/styling/MessageReactionsDetail.scss index 37c297df1..3ddfba238 100644 --- a/src/components/Reactions/styling/MessageReactionsDetail.scss +++ b/src/components/Reactions/styling/MessageReactionsDetail.scss @@ -11,40 +11,24 @@ .str-chat__message-reactions-detail { border-radius: var(--radius-lg); background: var(--background-core-elevation-2); - max-width: 278px; - min-width: min(90vw, 278px); + max-width: 256px; + min-width: min(90vw, 256px); &:has(.str-chat__reaction-selector-extended-list) { + @include common.clipping-fade; + @include utils.scrollable-y; + padding: 0; display: block; - @include utils.scrollable-y; scrollbar-width: none; - border-radius: var(--radius-lg); - max-height: 250px; - } - - &::after { - content: ''; - position: absolute; - width: 100%; - bottom: 0; - inset-inline-start: 0; - height: var(--size-16); - border-end-end-radius: inherit; - border-end-start-radius: inherit; - background: linear-gradient( - to bottom, - transparent 5%, - var(--background-core-elevation-0) 95% - ); + max-height: 320px; + max-width: unset; + min-width: unset; } font-family: var(--typography-font-family-sans); - box-shadow: - 0 0 0 1px rgba(0, 0, 0, 0.05), - 0 4px 8px 0 rgba(0, 0, 0, 0.14), - 0 12px 24px 0 rgba(0, 0, 0, 0.1); + box-shadow: var(--light-elevation-3); padding-block-start: var(--spacing-xxs); @@ -109,19 +93,21 @@ } } + .str-chat__message-reactions-detail__user-list-container { + position: relative; + border-radius: inherit; + @include common.clipping-fade; + &::before { + display: none; + } + } + .str-chat__message-reactions-detail__user-list { @include utils.scrollable-y; scrollbar-width: none; position: relative; padding-block-end: var(--spacing-xxs); - max-height: 100px; - - &:has( - .str-chat__message-reactions-detail__user-list-item - .str-chat__message-reactions-detail__user-list-item-button:nth-child(-n + 3) - ) { - max-height: 106px; - } + max-height: 180px; .str-chat__message-reactions-detail__skeleton-item { padding-block: var(--spacing-xxs); @@ -152,6 +138,7 @@ align-items: center; gap: var(--spacing-xs); padding-inline: calc(var(--spacing-sm) + var(--spacing-xxs)); + max-height: var(--size-40); .str-chat__message-reactions-detail__user-list-item-icon { font-family: system-ui; @@ -163,7 +150,6 @@ .str-chat__message-reactions-detail__user-list-item-info { display: flex; flex-direction: column; - gap: var(--spacing-xxxs); flex-grow: 1; min-width: 0; diff --git a/src/components/Reactions/styling/ReactionSelector.scss b/src/components/Reactions/styling/ReactionSelector.scss index 40bf42f99..5e6c00b2c 100644 --- a/src/components/Reactions/styling/ReactionSelector.scss +++ b/src/components/Reactions/styling/ReactionSelector.scss @@ -1,4 +1,5 @@ -@use '../../../styling/utils'; +@use '../../../styling/utils' as utils; +@use './common' as common; .str-chat__reaction-selector { display: flex; @@ -9,10 +10,9 @@ gap: var(--spacing-xs); border-radius: var(--radius-4xl, 32px); - border: 1px solid var(--border-core-surface-subtle, #e2e6ea); background: var(--background-core-elevation-2, #fff); - box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.16); + box-shadow: var(--light-elevation-3); &:has(.str-chat__reaction-selector-extended-list) { padding: 0; @@ -20,7 +20,9 @@ @include utils.scrollable-y; scrollbar-width: none; border-radius: var(--radius-lg); - max-height: 250px; + max-height: 320px; + + @include common.clipping-fade; } .str-chat__reaction-selector__add-button { diff --git a/src/components/Reactions/styling/common.scss b/src/components/Reactions/styling/common.scss index 617f178c7..4b5b9ca05 100644 --- a/src/components/Reactions/styling/common.scss +++ b/src/components/Reactions/styling/common.scss @@ -46,3 +46,38 @@ } } } + +@mixin clipping-fade() { + &::before, + &::after { + content: ''; + position: absolute; + width: 100%; + inset-inline-start: 0; + height: var(--size-16); + } + + &::after { + bottom: 0; + border-end-end-radius: inherit; + border-end-start-radius: inherit; + background: linear-gradient( + to bottom, + transparent 5%, + var(--background-core-elevation-0) 95% + ); + } + + &::before { + // TODO: figure out a better way (z-index isn't optimal) + z-index: 1; + top: 0; + border-start-end-radius: inherit; + border-start-start-radius: inherit; + background: linear-gradient( + to top, + transparent 5%, + var(--background-core-elevation-0) 95% + ); + } +}