Skip to content

Commit 67401b0

Browse files
authored
feat: Support custom rendering message menus and reactions (#854)
[CLNP-1217](https://sendbird.atlassian.net/browse/CLNP-1217) ### ChangeLog * Add props to the MessageContent component * `renderMessageMenu`, `renderEmojiMenu`, and `renderEmojiReactions` ### How to use these? ```tsx <Channel renderMessageContent={(props) => { return <MessageContent {...props} renderMessageMenu={(props) => { return <MessageMenu {...props> /> }} renderEmojiMenu={(props) => { return <MessageEmojiMenu {...props> /> }} renderEmojiReactions={(props) => { return <EmojiReactions {...props> /> }} /> }} /> ```
1 parent 5b91e7c commit 67401b0

File tree

5 files changed

+160
-156
lines changed

5 files changed

+160
-156
lines changed

src/ui/EmojiReactions/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { AddReactionBadgeItem } from './AddReactionBadgeItem';
1919
import { MobileEmojisBottomSheet } from '../MobileMenu/MobileEmojisBottomSheet';
2020
import { User } from '@sendbird/chat';
2121

22-
interface Props {
22+
export interface EmojiReactionsProps {
2323
className?: string | Array<string>;
2424
userId: string;
2525
message: SendableMessageType;
@@ -43,7 +43,7 @@ const EmojiReactions = ({
4343
isByMe = false,
4444
toggleReaction,
4545
onPressUserProfile,
46-
}: Props): ReactElement => {
46+
}: EmojiReactionsProps): ReactElement => {
4747
const { isMobile } = useMediaQueryContext();
4848
const addReactionRef = useRef(null);
4949
const [showEmojiList, setShowEmojiList] = useState(false);
Lines changed: 55 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,85 @@
1-
import React, { Dispatch, ReactElement, SetStateAction, useContext, useRef } from 'react';
1+
import React, {
2+
ReactElement,
3+
useContext,
4+
useRef,
5+
} from 'react';
26
import '../index.scss';
3-
import { getClassName, isSendableMessage, SendableMessageType } from '../../../utils';
7+
import { isSendableMessage } from '../../../utils';
48
import ContextMenu, { MenuItems } from '../../ContextMenu';
59
import Avatar from '../../Avatar';
610
import UserProfile from '../../UserProfile';
7-
import MessageItemMenu from '../../MessageItemMenu';
8-
import { ThreadReplySelectType } from '../../../modules/Channel/context/const';
9-
import MessageItemReactionMenu from '../../MessageItemReactionMenu';
1011
import { MessageContentProps } from '../index';
1112
import { UserProfileContext } from '../../../lib/UserProfileContext';
1213

1314
export interface MessageProfileProps extends MessageContentProps {
14-
setSupposedHover?: Dispatch<SetStateAction<boolean>>;
15-
isMobile?: boolean;
16-
isReactionEnabledInChannel?: boolean;
17-
isReactionEnabledClassName?: string;
1815
isByMe?: boolean;
19-
isByMeClassName?: string;
20-
useReplyingClassName?: string;
2116
displayThreadReplies?: boolean;
22-
supposedHoverClassName?: string;
2317
}
2418

25-
export default function MessageProfile(props: MessageProfileProps): ReactElement {
26-
const avatarRef = useRef(null);
27-
19+
export default function MessageProfile(
20+
props: MessageProfileProps,
21+
): ReactElement {
2822
const {
2923
message,
3024
channel,
3125
userId,
32-
disabled = false,
3326
chainBottom = false,
34-
replyType,
35-
threadReplySelectType,
36-
emojiContainer,
37-
scrollToMessage,
38-
showEdit,
39-
showRemove,
40-
resendMessage,
41-
toggleReaction,
42-
setQuoteMessage,
43-
onReplyInThread,
44-
45-
setSupposedHover,
46-
isMobile,
47-
isReactionEnabledInChannel,
48-
isReactionEnabledClassName,
4927
isByMe,
50-
isByMeClassName,
51-
useReplyingClassName,
5228
displayThreadReplies,
53-
supposedHoverClassName,
5429
} = props;
30+
const avatarRef = useRef(null);
5531

5632
const { disableUserProfile, renderUserProfile } = useContext(UserProfileContext);
5733

34+
if (isByMe || chainBottom || !isSendableMessage(message)) {
35+
return null;
36+
}
37+
5838
return (
59-
<div className={getClassName(['sendbird-message-content__left', isReactionEnabledClassName, isByMeClassName, useReplyingClassName])}>
60-
{(!isByMe && !chainBottom && isSendableMessage(message)) && (
61-
/** user profile */
62-
<ContextMenu
63-
menuTrigger={(toggleDropdown: () => void): ReactElement => (
64-
<Avatar
65-
className={`sendbird-message-content__left__avatar ${displayThreadReplies ? 'use-thread-replies' : ''}`}// @ts-ignore
66-
src={channel?.members?.find((member) => member?.userId === message.sender.userId)?.profileUrl || message.sender.profileUrl || ''}
67-
// TODO: Divide getting profileUrl logic to utils
68-
ref={avatarRef}
69-
width="28px"
70-
height="28px"
71-
onClick={(): void => { if (!disableUserProfile) toggleDropdown(); }}
72-
/>
73-
)}
74-
menuItems={(closeDropdown) => (
75-
<MenuItems
76-
/**
77-
* parentRef: For catching location(x, y) of MenuItems
78-
* parentContainRef: For toggling more options(menus & reactions)
79-
*/
80-
parentRef={avatarRef}
81-
parentContainRef={avatarRef}
82-
closeDropdown={closeDropdown}
83-
style={{ paddingTop: '0px', paddingBottom: '0px' }}
84-
>
85-
{renderUserProfile
86-
? renderUserProfile({ user: message.sender, close: closeDropdown, currentUserId: userId })
87-
: (<UserProfile user={message.sender} onSuccess={closeDropdown} />)
88-
}
89-
</MenuItems>
90-
)}
39+
<ContextMenu
40+
menuTrigger={(toggleDropdown: () => void): ReactElement => (
41+
<Avatar
42+
className={`sendbird-message-content__left__avatar ${
43+
displayThreadReplies ? 'use-thread-replies' : ''
44+
}`} // @ts-ignore
45+
src={
46+
channel?.members?.find(
47+
(member) => member?.userId === message.sender.userId,
48+
)?.profileUrl
49+
|| message.sender.profileUrl
50+
|| ''
51+
}
52+
// TODO: Divide getting profileUrl logic to utils
53+
ref={avatarRef}
54+
width="28px"
55+
height="28px"
56+
onClick={(): void => {
57+
if (!disableUserProfile) toggleDropdown();
58+
}}
9159
/>
9260
)}
93-
{/* outgoing menu */}
94-
{isByMe && !isMobile && (
95-
<div className={getClassName(['sendbird-message-content-menu', isReactionEnabledClassName, supposedHoverClassName, isByMeClassName])}>
96-
<MessageItemMenu
97-
channel={channel}
98-
message={message as SendableMessageType}
99-
isByMe={isByMe}
100-
replyType={replyType}
101-
disabled={disabled}
102-
showEdit={showEdit}
103-
showRemove={showRemove}
104-
resendMessage={resendMessage}
105-
setQuoteMessage={setQuoteMessage}
106-
setSupposedHover={setSupposedHover}
107-
onReplyInThread={({ message }) => {
108-
if (threadReplySelectType === ThreadReplySelectType.THREAD) {
109-
onReplyInThread({ message });
110-
} else if (threadReplySelectType === ThreadReplySelectType.PARENT) {
111-
scrollToMessage(message.parentMessage?.createdAt, message.parentMessageId);
112-
}
113-
}}
114-
/>
115-
{isReactionEnabledInChannel && (
116-
<MessageItemReactionMenu
117-
message={message as SendableMessageType}
118-
userId={userId}
119-
emojiContainer={emojiContainer}
120-
toggleReaction={toggleReaction}
121-
setSupposedHover={setSupposedHover}
122-
/>
61+
menuItems={(closeDropdown) => (
62+
<MenuItems
63+
/**
64+
* parentRef: For catching location(x, y) of MenuItems
65+
* parentContainRef: For toggling more options(menus & reactions)
66+
*/
67+
parentRef={avatarRef}
68+
parentContainRef={avatarRef}
69+
closeDropdown={closeDropdown}
70+
style={{ paddingTop: '0px', paddingBottom: '0px' }}
71+
>
72+
{renderUserProfile ? (
73+
renderUserProfile({
74+
user: message.sender,
75+
close: closeDropdown,
76+
currentUserId: userId,
77+
})
78+
) : (
79+
<UserProfile user={message.sender} onSuccess={closeDropdown} />
12380
)}
124-
</div>
81+
</MenuItems>
12582
)}
126-
</div>
83+
/>
12784
);
12885
}

src/ui/MessageContent/index.tsx

Lines changed: 91 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import format from 'date-fns/format';
77
import './index.scss';
88

99
import MessageStatus from '../MessageStatus';
10-
import MessageItemMenu from '../MessageItemMenu';
11-
import MessageItemReactionMenu from '../MessageItemReactionMenu';
10+
import { MessageMenu, MessageMenuProps } from '../MessageItemMenu';
11+
import { MessageEmojiMenu, MessageEmojiMenuProps } from '../MessageItemReactionMenu';
1212
import Label, { LabelTypography, LabelColors } from '../Label';
13-
import EmojiReactions from '../EmojiReactions';
13+
import EmojiReactions, { EmojiReactionsProps } from '../EmojiReactions';
1414

1515
import ClientAdminMessage from '../AdminMessage';
1616
import QuoteMessage from '../QuoteMessage';
@@ -69,6 +69,9 @@ export interface MessageContentProps {
6969
renderSenderProfile?: (props: MessageProfileProps) => ReactNode;
7070
renderMessageBody?: (props: MessageBodyProps) => ReactNode;
7171
renderMessageHeader?: (props: MessageHeaderProps) => ReactNode;
72+
renderMessageMenu?: (props: MessageMenuProps) => ReactNode;
73+
renderEmojiMenu?: (props: MessageEmojiMenuProps) => ReactNode;
74+
renderEmojiReactions?: (props: EmojiReactionsProps) => ReactNode;
7275
}
7376

7477
export default function MessageContent(props: MessageContentProps): ReactElement {
@@ -109,6 +112,15 @@ export default function MessageContent(props: MessageContentProps): ReactElement
109112
renderMessageHeader = (props: MessageHeaderProps) => (
110113
<MessageHeader {...props}/>
111114
),
115+
renderMessageMenu = (props: MessageMenuProps) => (
116+
<MessageMenu {...props} />
117+
),
118+
renderEmojiMenu = (props: MessageEmojiMenuProps) => (
119+
<MessageEmojiMenu {...props} />
120+
),
121+
renderEmojiReactions = (props: EmojiReactionsProps) => (
122+
<EmojiReactions {...props} />
123+
),
112124
} = props;
113125

114126
const { dateLocale } = useLocalization();
@@ -165,20 +177,48 @@ export default function MessageContent(props: MessageContentProps): ReactElement
165177
onMouseLeave={() => setMouseHover(false)}
166178
>
167179
{/* left */}
168-
{
169-
renderSenderProfile({
170-
...props,
171-
setSupposedHover,
172-
isMobile,
173-
isReactionEnabledInChannel,
174-
isReactionEnabledClassName,
175-
isByMe,
176-
isByMeClassName,
177-
useReplyingClassName,
178-
displayThreadReplies,
179-
supposedHoverClassName,
180-
})
181-
}
180+
<div className={getClassName(['sendbird-message-content__left', isReactionEnabledClassName, isByMeClassName, useReplyingClassName])}>
181+
{
182+
renderSenderProfile({
183+
...props,
184+
isByMe,
185+
displayThreadReplies,
186+
})
187+
}
188+
{/* outgoing menu */}
189+
{isByMe && !isMobile && (
190+
<div className={getClassName(['sendbird-message-content-menu', isReactionEnabledClassName, supposedHoverClassName, isByMeClassName])}>
191+
{renderMessageMenu({
192+
channel: channel,
193+
message: message as SendableMessageType,
194+
isByMe: isByMe,
195+
replyType: replyType,
196+
disabled: disabled,
197+
showEdit: showEdit,
198+
showRemove: showRemove,
199+
resendMessage: resendMessage,
200+
setQuoteMessage: setQuoteMessage,
201+
setSupposedHover: setSupposedHover,
202+
onReplyInThread: ({ message }) => {
203+
if (threadReplySelectType === ThreadReplySelectType.THREAD) {
204+
onReplyInThread({ message });
205+
} else if (threadReplySelectType === ThreadReplySelectType.PARENT) {
206+
scrollToMessage(message.parentMessage?.createdAt, message.parentMessageId);
207+
}
208+
},
209+
})}
210+
{isReactionEnabledInChannel && (
211+
renderEmojiMenu({
212+
message: message as SendableMessageType,
213+
userId: userId,
214+
emojiContainer: emojiContainer,
215+
toggleReaction: toggleReaction,
216+
setSupposedHover: setSupposedHover,
217+
})
218+
)}
219+
</div>
220+
)}
221+
</div>
182222
{/* middle */}
183223
<div
184224
className={'sendbird-message-content__middle'}
@@ -247,16 +287,18 @@ export default function MessageContent(props: MessageContentProps): ReactElement
247287
? '' : 'primary',
248288
mouseHover ? 'mouse-hover' : '',
249289
])}>
250-
<EmojiReactions
251-
userId={userId}
252-
message={message as SendableMessageType}
253-
channel={channel}
254-
isByMe={isByMe}
255-
emojiContainer={emojiContainer}
256-
memberNicknamesMap={nicknamesMap}
257-
toggleReaction={toggleReaction}
258-
onPressUserProfile={onPressUserProfileHandler}
259-
/>
290+
{
291+
renderEmojiReactions({
292+
userId,
293+
message: message as SendableMessageType,
294+
channel,
295+
isByMe,
296+
emojiContainer,
297+
memberNicknamesMap: nicknamesMap,
298+
toggleReaction,
299+
onPressUserProfile: onPressUserProfileHandler,
300+
})
301+
}
260302
</div>
261303
)}
262304
{/* message timestamp when sent by others */}
@@ -283,38 +325,37 @@ export default function MessageContent(props: MessageContentProps): ReactElement
283325
</div>
284326
{/* right */}
285327
<div className={getClassName(['sendbird-message-content__right', chainTopClassName, isReactionEnabledClassName, useReplyingClassName])}>
286-
{/* incoming menu */}
287328
{!isByMe && !isMobile && (
288329
<div className={getClassName(['sendbird-message-content-menu', chainTopClassName, supposedHoverClassName, isByMeClassName])}>
289330
{isReactionEnabledInChannel && (
290-
<MessageItemReactionMenu
291-
className="sendbird-message-content-menu__reaction-menu"
292-
message={message as SendableMessageType}
293-
userId={userId}
294-
emojiContainer={emojiContainer}
295-
toggleReaction={toggleReaction}
296-
setSupposedHover={setSupposedHover}
297-
/>
331+
renderEmojiMenu({
332+
className: 'sendbird-message-content-menu__reaction-menu',
333+
message: message as SendableMessageType,
334+
userId: userId,
335+
emojiContainer: emojiContainer,
336+
toggleReaction: toggleReaction,
337+
setSupposedHover: setSupposedHover,
338+
})
298339
)}
299-
<MessageItemMenu
300-
className="sendbird-message-content-menu__normal-menu"
301-
channel={channel}
302-
message={message as SendableMessageType}
303-
isByMe={isByMe}
304-
replyType={replyType}
305-
disabled={disabled}
306-
showRemove={showRemove}
307-
resendMessage={resendMessage}
308-
setQuoteMessage={setQuoteMessage}
309-
setSupposedHover={setSupposedHover}
310-
onReplyInThread={({ message }) => {
340+
{renderMessageMenu({
341+
className: 'sendbird-message-content-menu__normal-menu',
342+
channel,
343+
message: message as SendableMessageType,
344+
isByMe,
345+
replyType,
346+
disabled,
347+
showRemove,
348+
resendMessage,
349+
setQuoteMessage,
350+
setSupposedHover,
351+
onReplyInThread: ({ message }) => {
311352
if (threadReplySelectType === ThreadReplySelectType.THREAD) {
312353
onReplyInThread({ message });
313354
} else if (threadReplySelectType === ThreadReplySelectType.PARENT) {
314355
scrollToMessage(message.parentMessage?.createdAt, message.parentMessageId);
315356
}
316-
}}
317-
/>
357+
},
358+
})}
318359
</div>
319360
)}
320361
</div>

0 commit comments

Comments
 (0)