Skip to content

Commit 58532ab

Browse files
authored
refactor: Apply message menu to the Thread module (#1174)
[CLNP-4083](https://sendbird.atlassian.net/browse/CLNP-4083) ## ChangeLog * Supported message menu customization in Thread * Added `renderMessageMenu` and `renderEmojiMenu` props to the `<ParentMessageInfo />`, `<ThreadListItem />`, and `<ThreadListItemContent />` components. How to use? ```tsx <Thread renderMessage={(props) => <ThreadListItem {...props} renderMessageMenu={(props) => ( // render your custom message menu here <MessageMenu {...props} renderMenuItems={({ items }) => ( <> <items.CopyMenuItem /> <items.DeleteMenuItem /> </> )} /> )} />} /> ```
1 parent a69c501 commit 58532ab

File tree

9 files changed

+152
-228
lines changed

9 files changed

+152
-228
lines changed

src/modules/GroupChannel/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import GroupChannelUI, { GroupChannelUIProps } from './components/GroupChannelUI
66
export interface GroupChannelProps extends GroupChannelProviderProps, GroupChannelUIProps { }
77
export const GroupChannel = (props: GroupChannelProps) => {
88
return (
9-
<GroupChannelProvider {...props}>
9+
<GroupChannelProvider {...props} >
1010
<GroupChannelUI {...props} />
1111
</GroupChannelProvider>
1212
);

src/modules/Thread/components/ParentMessageInfo/index.scss

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,19 +86,9 @@
8686
height: 100%;
8787
}
8888

89+
.sendbird-parent-message-info__reaction-menu,
8990
.sendbird-parent-message-info__context-menu {
90-
position: absolute;
91-
top: 6px;
92-
right: 12px;
93-
display: none;
94-
}
95-
.sendbird-parent-message-info__context-menu.use-reaction {
96-
right: 44px;
97-
}
98-
.sendbird-parent-message-info__reaction-menu {
99-
position: absolute;
100-
top: 6px;
101-
right: 12px;
91+
position: relative;
10292
display: none;
10393
}
10494

@@ -112,13 +102,21 @@
112102
}
113103

114104
// display menus
105+
.sendbird-parent-message-info__menu-container,
115106
.sendbird-parent-message-info:hover .sendbird-parent-message-info__context-menu,
116107
.sendbird-parent-message-info:hover .sendbird-parent-message-info__reaction-menu,
117108
.sendbird-parent-message-info__context-menu.sendbird-mouse-hover,
118109
.sendbird-parent-message-info__reaction-menu.sendbird-mouse-hover {
119110
display: inline-flex;
120111
}
121112

113+
.sendbird-parent-message-info__menu-container {
114+
flex-direction: row;
115+
position: absolute;
116+
top: 6px;
117+
right: 12px;
118+
}
119+
122120
// background color
123121
.sendbird-parent-message-info .sendbird-parent-message-info__content__body {
124122
@include themed() {

src/modules/Thread/components/ParentMessageInfo/index.tsx

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useContext, useEffect, useRef, useState } from 'react';
1+
import React, { ReactNode, useContext, useEffect, useRef, useState } from 'react';
22
import format from 'date-fns/format';
33
import { FileMessage } from '@sendbird/chat/message';
44

@@ -17,9 +17,8 @@ import SuggestedMentionList from '../SuggestedMentionList';
1717
import Avatar from '../../../../ui/Avatar';
1818
import Label, { LabelTypography, LabelColors } from '../../../../ui/Label';
1919
import FileViewer from '../../../../ui/FileViewer';
20-
import MessageItemMenu from '../../../../ui/MessageItemMenu';
21-
import MessageItemReactionMenu from '../../../../ui/MessageItemReactionMenu';
22-
import ContextMenu, { MenuItems } from '../../../../ui/ContextMenu';
20+
import { MessageEmojiMenu, MessageEmojiMenuProps } from '../../../../ui/MessageItemReactionMenu';
21+
import ContextMenu, { EMOJI_MENU_ROOT_ID, getObservingId, MENU_OBSERVING_CLASS_NAME, MENU_ROOT_ID, MenuItems } from '../../../../ui/ContextMenu';
2322
import ConnectedUserProfile from '../../../../ui/UserProfile';
2423
import MessageInput from '../../../../ui/MessageInput';
2524
import { MessageInputKeys } from '../../../../ui/MessageInput/const';
@@ -31,13 +30,19 @@ import { useDirtyGetMentions } from '../../../Message/hooks/useDirtyGetMentions'
3130
import { User } from '@sendbird/chat';
3231
import { getCaseResolvedReplyType } from '../../../../lib/utils/resolvedReplyType';
3332
import { classnames } from '../../../../utils/utils';
33+
import { MessageMenu, MessageMenuProps } from '../../../../ui/MessageMenu';
34+
import useElementObserver from '../../../../hooks/useElementObserver';
3435

3536
export interface ParentMessageInfoProps {
3637
className?: string;
38+
renderEmojiMenu?: (props: MessageEmojiMenuProps) => ReactNode;
39+
renderMessageMenu?: (props: MessageMenuProps) => ReactNode;
3740
}
3841

3942
export default function ParentMessageInfo({
4043
className,
44+
renderEmojiMenu = (props) => <MessageEmojiMenu {...props} />,
45+
renderMessageMenu = (props) => <MessageMenu {...props} />,
4146
}: ParentMessageInfoProps): React.ReactElement {
4247
const { stores, config } = useSendbirdStateContext();
4348
const { isOnline, userMention, logger, groupChannel } = config;
@@ -59,8 +64,14 @@ export default function ParentMessageInfo({
5964
} = useThreadContext();
6065
const { isMobile } = useMediaQueryContext();
6166

67+
const isMenuMounted = useElementObserver(
68+
`#${getObservingId(parentMessage.messageId)}.${MENU_OBSERVING_CLASS_NAME}`,
69+
[
70+
document.getElementById(MENU_ROOT_ID),
71+
document.getElementById(EMOJI_MENU_ROOT_ID),
72+
],
73+
);
6274
const [showRemove, setShowRemove] = useState(false);
63-
const [supposedHover, setSupposedHover] = useState(false);
6475
const [showFileViewer, setShowFileViewer] = useState(false);
6576
const isReactionEnabled = getIsReactionEnabled({
6677
channel: currentChannel,
@@ -301,30 +312,33 @@ export default function ParentMessageInfo({
301312
</div>
302313
{/* context menu */}
303314
{!isMobile && (
304-
<MessageItemMenu
305-
className={classnames('sendbird-parent-message-info__context-menu', isReactionEnabled && 'use-reaction', supposedHover && 'sendbird-mouse-hover')}
306-
channel={currentChannel}
307-
message={parentMessage}
308-
isByMe={userId === parentMessage?.sender?.userId}
309-
disableDeleteMessage={allThreadMessages.length > 0}
310-
replyType={replyType}
311-
showEdit={setShowEditInput}
312-
showRemove={setShowRemove}
313-
setSupposedHover={setSupposedHover}
314-
onMoveToParentMessage={() => {
315-
onMoveToParentMessage?.({ message: parentMessage, channel: currentChannel });
316-
}}
317-
deleteMessage={deleteMessage}
318-
/>
319-
)}
320-
{(isReactionEnabled && !isMobile) && (
321-
<MessageItemReactionMenu
322-
className={classnames('sendbird-parent-message-info__reaction-menu', supposedHover && 'sendbird-mouse-hover')}
323-
message={parentMessage}
324-
userId={userId}
325-
emojiContainer={emojiContainer}
326-
toggleReaction={toggleReaction}
327-
/>
315+
<div className='sendbird-parent-message-info__menu-container'>
316+
{
317+
renderMessageMenu({
318+
className: classnames('sendbird-parent-message-info__context-menu', isReactionEnabled && 'use-reaction', isMenuMounted && 'sendbird-mouse-hover'),
319+
channel: currentChannel,
320+
message: parentMessage,
321+
isByMe: userId === parentMessage?.sender?.userId,
322+
disableDeleteMessage: allThreadMessages.length > 0,
323+
replyType: replyType,
324+
showEdit: setShowEditInput,
325+
showRemove: setShowRemove,
326+
onMoveToParentMessage: () => {
327+
onMoveToParentMessage?.({ message: parentMessage, channel: currentChannel });
328+
},
329+
deleteMessage: deleteMessage,
330+
})
331+
}
332+
{isReactionEnabled && (
333+
renderEmojiMenu({
334+
className: classnames('sendbird-parent-message-info__reaction-menu', isMenuMounted && 'sendbird-mouse-hover'),
335+
message: parentMessage,
336+
userId: userId,
337+
emojiContainer: emojiContainer,
338+
toggleReaction: toggleReaction,
339+
})
340+
)}
341+
</div>
328342
)}
329343
{showRemove && (
330344
<RemoveMessage

src/modules/Thread/components/ThreadList/ThreadListItem.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo, useState, useRef, useEffect, useLayoutEffect } from 'react';
1+
import React, { useMemo, useState, useRef, useEffect, useLayoutEffect, ReactNode } from 'react';
22
import format from 'date-fns/format';
33
import type { FileMessage, MultipleFilesMessage } from '@sendbird/chat/message';
44

@@ -21,6 +21,8 @@ import { SendableMessageType } from '../../../../utils';
2121
import { User } from '@sendbird/chat';
2222
import { getCaseResolvedReplyType } from '../../../../lib/utils/resolvedReplyType';
2323
import { classnames } from '../../../../utils/utils';
24+
import type { MessageMenuProps } from '../../../../ui/MessageMenu';
25+
import type { MessageEmojiMenuProps } from '../../../../ui/MessageItemReactionMenu';
2426

2527
export interface ThreadListItemProps {
2628
className?: string;
@@ -30,6 +32,8 @@ export interface ThreadListItemProps {
3032
hasSeparator?: boolean;
3133
renderCustomSeparator?: (props: { message: SendableMessageType }) => React.ReactElement;
3234
handleScroll?: () => void;
35+
renderEmojiMenu?: (props: MessageEmojiMenuProps) => ReactNode;
36+
renderMessageMenu?: (props: MessageMenuProps) => ReactNode;
3337
}
3438

3539
export default function ThreadListItem({
@@ -40,6 +44,8 @@ export default function ThreadListItem({
4044
hasSeparator,
4145
renderCustomSeparator,
4246
handleScroll,
47+
renderEmojiMenu,
48+
renderMessageMenu,
4349
}: ThreadListItemProps): React.ReactElement {
4450
const { stores, config } = useSendbirdStateContext();
4551
const { isOnline, userMention, logger, groupChannel } = config;
@@ -247,6 +253,8 @@ export default function ThreadListItem({
247253
showFileViewer={setShowFileViewer}
248254
toggleReaction={toggleReaction}
249255
showEdit={setShowEdit}
256+
renderEmojiMenu={renderEmojiMenu}
257+
renderMessageMenu={renderMessageMenu}
250258
/>
251259
{/* modal */}
252260
{showRemove && (

src/modules/Thread/components/ThreadList/ThreadListItemContent.tsx

Lines changed: 57 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
import React, { useContext, useRef, useState } from 'react';
1+
import React, { ReactNode, useContext, useRef, useState } from 'react';
22
import { EmojiContainer } from '@sendbird/chat';
33
import { FileMessage, MultipleFilesMessage, UserMessage } from '@sendbird/chat/message';
44
import { GroupChannel } from '@sendbird/chat/groupChannel';
55

66
import './ThreadListItemContent.scss';
77

88
import { ReplyType } from '../../../../types';
9-
import ContextMenu, { MenuItems } from '../../../../ui/ContextMenu';
9+
import ContextMenu, { EMOJI_MENU_ROOT_ID, getObservingId, MENU_OBSERVING_CLASS_NAME, MENU_ROOT_ID, MenuItems } from '../../../../ui/ContextMenu';
1010
import Avatar from '../../../../ui/Avatar';
1111
import { UserProfileContext } from '../../../../lib/UserProfileContext';
1212
import UserProfile from '../../../../ui/UserProfile';
13-
import MessageItemMenu from '../../../../ui/MessageItemMenu';
14-
import MessageItemReactionMenu from '../../../../ui/MessageItemReactionMenu';
13+
import { MessageEmojiMenu, MessageEmojiMenuProps } from '../../../../ui/MessageItemReactionMenu';
1514
import Label, { LabelTypography, LabelColors } from '../../../../ui/Label';
1615
import {
1716
getClassName,
@@ -44,12 +43,15 @@ import { useThreadMessageKindKeySelector } from '../../../Channel/context/hooks/
4443
import { useFileInfoListWithUploaded } from '../../../Channel/context/hooks/useFileInfoListWithUploaded';
4544
import { useThreadContext } from '../../context/ThreadProvider';
4645
import { classnames } from '../../../../utils/utils';
46+
import { MessageMenu, MessageMenuProps } from '../../../../ui/MessageMenu';
47+
import useElementObserver from '../../../../hooks/useElementObserver';
4748

4849
export interface ThreadListItemContentProps {
4950
className?: string;
5051
userId: string;
5152
channel: GroupChannel;
5253
message: SendableMessageType;
54+
/** @deprecated This prop is deprecated and no longer in use. */
5355
disabled?: boolean;
5456
chainTop?: boolean;
5557
chainBottom?: boolean;
@@ -65,14 +67,15 @@ export interface ThreadListItemContentProps {
6567
resendMessage?: (message: SendableMessageType) => void;
6668
toggleReaction?: (message: SendableMessageType, reactionKey: string, isReacted: boolean) => void;
6769
onReplyInThread?: (props: { message: SendableMessageType }) => void;
70+
renderEmojiMenu?: (props: MessageEmojiMenuProps) => ReactNode;
71+
renderMessageMenu?: (props: MessageMenuProps) => ReactNode;
6872
}
6973

7074
export default function ThreadListItemContent({
7175
className,
7276
userId,
7377
channel,
7478
message,
75-
disabled = false,
7679
chainTop = false,
7780
chainBottom = false,
7881
isMentionEnabled = false,
@@ -87,14 +90,22 @@ export default function ThreadListItemContent({
8790
resendMessage,
8891
toggleReaction,
8992
onReplyInThread,
93+
renderEmojiMenu = (props) => <MessageEmojiMenu {...props} />,
94+
renderMessageMenu = (props) => <MessageMenu {...props} />,
9095
}: ThreadListItemContentProps): React.ReactElement {
9196
const messageTypes = getUIKitMessageTypes();
9297
const { isMobile } = useMediaQueryContext();
9398
const { dateLocale } = useLocalization();
9499
const { config, eventHandlers } = useSendbirdStateContext?.() || {};
95100
const { logger } = config;
96101
const onPressUserProfileHandler = eventHandlers?.reaction?.onPressUserProfile;
97-
const [supposedHover, setSupposedHover] = useState(false);
102+
const isMenuMounted = useElementObserver(
103+
`#${getObservingId(message.messageId)}.${MENU_OBSERVING_CLASS_NAME}`,
104+
[
105+
document.getElementById(MENU_ROOT_ID),
106+
document.getElementById(EMOJI_MENU_ROOT_ID),
107+
],
108+
);
98109
const { disableUserProfile, renderUserProfile } = useContext(UserProfileContext);
99110
const { deleteMessage, onBeforeDownloadFileMessage } = useThreadContext();
100111
const avatarRef = useRef(null);
@@ -106,7 +117,7 @@ export default function ThreadListItemContent({
106117
&& message?.parentMessageId && message?.parentMessage
107118
&& !disableQuoteMessage
108119
);
109-
const supposedHoverClassName = supposedHover ? 'sendbird-mouse-hover' : '';
120+
const supposedHoverClassName = isMenuMounted ? 'sendbird-mouse-hover' : '';
110121
const isReactionEnabledInChannel = isReactionEnabled && !channel?.isEphemeral;
111122
const isOgMessageEnabledInGroupChannel = channel.isGroupChannel() && config.groupChannel.enableOgtag;
112123

@@ -185,28 +196,28 @@ export default function ThreadListItemContent({
185196
supposedHoverClassName,
186197
)}
187198
>
188-
<MessageItemMenu
189-
className="sendbird-thread-list-item-content-menu__normal-menu"
190-
channel={channel}
191-
message={message as SendableMessageType}
192-
isByMe={isByMe}
193-
replyType={replyType}
194-
disabled={disabled}
195-
showEdit={showEdit}
196-
showRemove={showRemove}
197-
resendMessage={resendMessage}
198-
setSupposedHover={setSupposedHover}
199-
onReplyInThread={onReplyInThread}
200-
deleteMessage={deleteMessage}
201-
/>
199+
{renderMessageMenu({
200+
className: 'sendbird-thread-list-item-content-menu__normal-menu',
201+
channel: channel,
202+
message: message as SendableMessageType,
203+
isByMe: isByMe,
204+
replyType: replyType,
205+
showEdit: showEdit,
206+
showRemove: showRemove,
207+
resendMessage: resendMessage,
208+
onReplyInThread: onReplyInThread,
209+
deleteMessage: deleteMessage,
210+
})}
202211
{isReactionEnabledInChannel && (
203-
<MessageItemReactionMenu
204-
className="sendbird-thread-list-item-content-menu__reaction-menu"
205-
message={message as SendableMessageType}
206-
userId={userId}
207-
emojiContainer={emojiContainer}
208-
toggleReaction={toggleReaction}
209-
/>
212+
<>
213+
{renderEmojiMenu({
214+
className: 'sendbird-thread-list-item-content-menu__reaction-menu',
215+
message: message as SendableMessageType,
216+
userId: userId,
217+
emojiContainer: emojiContainer,
218+
toggleReaction: toggleReaction,
219+
})}
220+
</>
210221
)}
211222
</div>
212223
)}
@@ -352,27 +363,25 @@ export default function ThreadListItemContent({
352363
{(!isByMe && !isMobile) && (
353364
<div className={`sendbird-thread-list-item-content-menu ${supposedHoverClassName}`}>
354365
{isReactionEnabledInChannel && (
355-
<MessageItemReactionMenu
356-
className="sendbird-thread-list-item-content-menu__reaction-menu"
357-
message={message as SendableMessageType}
358-
userId={userId}
359-
emojiContainer={emojiContainer}
360-
toggleReaction={toggleReaction}
361-
/>
366+
renderEmojiMenu({
367+
className: 'sendbird-thread-list-item-content-menu__reaction-menu',
368+
message: message as SendableMessageType,
369+
userId: userId,
370+
emojiContainer: emojiContainer,
371+
toggleReaction: toggleReaction,
372+
})
362373
)}
363-
<MessageItemMenu
364-
className="sendbird-thread-list-item-content-menu__normal-menu"
365-
channel={channel}
366-
message={message as SendableMessageType}
367-
isByMe={isByMe}
368-
replyType={replyType}
369-
disabled={disabled}
370-
showRemove={showRemove}
371-
resendMessage={resendMessage}
372-
setSupposedHover={setSupposedHover}
373-
onReplyInThread={onReplyInThread}
374-
deleteMessage={deleteMessage}
375-
/>
374+
{renderMessageMenu({
375+
className: 'sendbird-thread-list-item-content-menu__normal-menu',
376+
channel: channel,
377+
message: message as SendableMessageType,
378+
isByMe: isByMe,
379+
replyType: replyType,
380+
showRemove: showRemove,
381+
resendMessage: resendMessage,
382+
onReplyInThread: onReplyInThread,
383+
deleteMessage: deleteMessage,
384+
})}
376385
</div>
377386
)}
378387
</div>

0 commit comments

Comments
 (0)