1- import React , { type ComponentPropsWithoutRef , useState } from 'react' ;
1+ import React , {
2+ type ComponentPropsWithoutRef ,
3+ type ComponentRef ,
4+ useMemo ,
5+ useRef ,
6+ useState ,
7+ } from 'react' ;
28import clsx from 'clsx' ;
39
4- import type { ReactionsListModalProps } from './ReactionsListModal' ;
510import { ReactionsListModal as DefaultReactionsListModal } from './ReactionsListModal' ;
611import { useProcessReactions } from './hooks/useProcessReactions' ;
712import type { MessageContextValue } from '../../context' ;
8- import { useComponentContext , useTranslationContext } from '../../context' ;
13+ import {
14+ useComponentContext ,
15+ useMessageContext ,
16+ useTranslationContext ,
17+ } from '../../context' ;
918
1019import { MAX_MESSAGE_REACTIONS_TO_FETCH } from '../Message/hooks' ;
1120
@@ -16,6 +25,7 @@ import type {
1625 ReactionsComparator ,
1726 ReactionType ,
1827} from './types' ;
28+ import { DialogAnchor , useDialogOnNearestManager } from '../Dialog' ;
1929
2030export type ReactionsListProps = Partial <
2131 Pick < MessageContextValue , 'handleFetchReactions' | 'reactionDetailsSort' >
@@ -53,6 +63,9 @@ export type ReactionsListProps = Partial<
5363 visualStyle ?: 'clustered' | 'segmented' | null ;
5464} ;
5565
66+ /**
67+ * Renders a button if `buttonIf` is true, otherwise renders a fragment. No props but children are passed to fragment, but all props are passed to button if it's rendered.
68+ */
5669const FragmentOrButton = ( {
5770 buttonIf : renderButton = false ,
5871 children,
@@ -77,22 +90,47 @@ const UnMemoizedReactionsList = (props: ReactionsListProps) => {
7790 ...rest
7891 } = props ;
7992
80- const { existingReactions, hasReactions, totalReactionCount } =
93+ const { existingReactions, hasReactions, totalReactionCount, uniqueReactionTypeCount } =
8194 useProcessReactions ( rest ) ;
8295 const [ selectedReactionType , setSelectedReactionType ] = useState < ReactionType | null > (
8396 null ,
8497 ) ;
8598 const { t } = useTranslationContext ( 'ReactionsList' ) ;
8699 const { ReactionsListModal = DefaultReactionsListModal } = useComponentContext ( ) ;
100+ const { isMyMessage, message } = useMessageContext ( 'ReactionsList' ) ;
101+
102+ const divRef = useRef < ComponentRef < 'div' > > ( null ) ;
103+ const dialogId = `message-reactions-detail-${ message . id } ` ;
104+ const { dialog, dialogManager } = useDialogOnNearestManager ( { id : dialogId } ) ;
87105
88- const handleReactionButtonClick = ( reactionType : ReactionType ) => {
106+ const handleReactionButtonClick = ( reactionType : ReactionType | null ) => {
89107 if ( totalReactionCount > MAX_MESSAGE_REACTIONS_TO_FETCH ) {
90108 return ;
91109 }
92110
93111 setSelectedReactionType ( reactionType ) ;
112+
113+ dialog . open ( ) ;
94114 } ;
95115
116+ /**
117+ * In segmented style with top position we show max 4 reactions and a
118+ * count of the rest, so we need to cap the existing reactions to display
119+ * at 4 and calculate the count of the rest.
120+ */
121+ const cappedExistingReactions = useMemo ( ( ) => {
122+ if ( visualStyle !== 'segmented' || verticalPosition !== 'top' ) return null ;
123+
124+ const sliced = existingReactions . slice ( 0 , 4 ) ;
125+ return {
126+ reactionCountToDisplay : sliced . reduce (
127+ ( accumulatedCount , { reactionCount } ) => accumulatedCount + reactionCount ,
128+ 0 ,
129+ ) ,
130+ reactionsToDisplay : sliced ,
131+ } ;
132+ } , [ existingReactions , verticalPosition , visualStyle ] ) ;
133+
96134 if ( ! hasReactions ) return null ;
97135
98136 return (
@@ -106,17 +144,18 @@ const UnMemoizedReactionsList = (props: ReactionsListProps) => {
106144 [ `str-chat__message-reactions--${ visualStyle } ` ] :
107145 typeof visualStyle === 'string' ,
108146 } ) }
147+ ref = { divRef }
109148 role = 'figure'
110149 >
111150 < FragmentOrButton
112151 buttonIf = { visualStyle === 'clustered' }
113152 className = 'str-chat__message-reactions__list-button'
114153 onClick = { ( ) =>
115- setSelectedReactionType ( existingReactions [ 0 ] ?. reactionType ?? null )
154+ handleReactionButtonClick ( existingReactions [ 0 ] ?. reactionType ?? null )
116155 }
117156 >
118157 < ul className = 'str-chat__message-reactions__list' >
119- { existingReactions . map (
158+ { ( cappedExistingReactions ?. reactionsToDisplay ?? existingReactions ) . map (
120159 ( { EmojiComponent, reactionCount, reactionType } ) =>
121160 EmojiComponent && (
122161 < li
@@ -128,12 +167,12 @@ const UnMemoizedReactionsList = (props: ReactionsListProps) => {
128167 className = 'str-chat__message-reactions__list-item-button'
129168 onClick = { ( ) => handleReactionButtonClick ( reactionType ) }
130169 >
131- < span className = 'str-chat__message-reactions__item -icon' >
170+ < span className = 'str-chat__message-reactions__list-item -icon' >
132171 < EmojiComponent />
133172 </ span >
134- { visualStyle === 'segmented' && (
173+ { visualStyle === 'segmented' && reactionCount > 1 && (
135174 < span
136- className = 'str-chat__message-reactions__item -count'
175+ className = 'str-chat__message-reactions__list-item -count'
137176 data-testclass = 'message-reactions-item-count'
138177 >
139178 { reactionCount }
@@ -143,6 +182,22 @@ const UnMemoizedReactionsList = (props: ReactionsListProps) => {
143182 </ li >
144183 ) ,
145184 ) }
185+ { uniqueReactionTypeCount > 4 && cappedExistingReactions && (
186+ < li className = 'str-chat__message-reactions__list-item str-chat__message-reactions__list-item--more' >
187+ < button
188+ className = 'str-chat__message-reactions__list-item-button'
189+ onClick = { ( ) =>
190+ handleReactionButtonClick (
191+ existingReactions . at ( - 1 ) ?. reactionType ?? null ,
192+ )
193+ }
194+ >
195+ < span className = 'str-chat__message-reactions__overflow-count' >
196+ +{ totalReactionCount - cappedExistingReactions . reactionCountToDisplay }
197+ </ span >
198+ </ button >
199+ </ li >
200+ ) }
146201 </ ul >
147202 { visualStyle === 'clustered' && (
148203 < span className = 'str-chat__message-reactions__total-count' >
@@ -151,19 +206,25 @@ const UnMemoizedReactionsList = (props: ReactionsListProps) => {
151206 ) }
152207 </ FragmentOrButton >
153208 </ div >
154- { selectedReactionType !== null && (
209+
210+ < DialogAnchor
211+ dialogManagerId = { dialogManager ?. id }
212+ id = { dialogId }
213+ offset = { 8 }
214+ placement = { isMyMessage ( ) ? 'bottom-end' : 'bottom-start' }
215+ referenceElement = { divRef . current }
216+ trapFocus
217+ updatePositionOnContentResize
218+ >
155219 < ReactionsListModal
156220 handleFetchReactions = { handleFetchReactions }
157- onClose = { ( ) => setSelectedReactionType ( null ) }
158- onSelectedReactionTypeChange = {
159- setSelectedReactionType as ReactionsListModalProps [ 'onSelectedReactionTypeChange' ]
160- }
161- open = { selectedReactionType !== null }
221+ onSelectedReactionTypeChange = { setSelectedReactionType }
162222 reactions = { existingReactions }
163223 selectedReactionType = { selectedReactionType }
164224 sortReactionDetails = { sortReactionDetails }
225+ totalReactionCount = { totalReactionCount }
165226 />
166- ) }
227+ </ DialogAnchor >
167228 </ >
168229 ) ;
169230} ;
0 commit comments