Skip to content

Commit 82e6d48

Browse files
authored
fix: Add missing props renderMessageContent (#921)
### Issue The `renderMessage` and the `renderMessageContent` props are diffrent. Customers can easily customize the `renderMessageContent` after we provide several sub-components of the `MessageContent` component. ### Change Log * Add missing props `renderMessageContent` in Channel ### Fix * Add missing props `renderMessageContent` in Channel * Create a util function, `omitObjectProperties` #### How to customize MessageContent in Channel? 1. Customize with `renderMessage` ```tsx <Channel renderMessage={(props) => ( <Message {...props} renderMessageContent={(props) => ( <MessageContent {...props} /> )} /> )} /> ``` 2. **[More Simple Way]** Customize with `renderMessageContent` ```tsx <Channel renderMessageContent={(props) => ( <MessageContent {...props} /> )} /> ```
1 parent ecc500b commit 82e6d48

File tree

7 files changed

+113
-66
lines changed

7 files changed

+113
-66
lines changed

src/modules/Channel/components/ChannelUI/index.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
import './channel-ui.scss';
22

33
import React from 'react';
4-
import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext';
54

5+
import type { MessageContentProps } from '../../../../ui/MessageContent';
6+
import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext';
67
import { useChannelContext } from '../../context/ChannelProvider';
78
import PlaceHolder, { PlaceHolderTypes } from '../../../../ui/PlaceHolder';
89
import ConnectionStatus from '../../../../ui/ConnectionStatus';
910
import ChannelHeader from '../ChannelHeader';
1011
import MessageList from '../MessageList';
1112
import TypingIndicator from '../TypingIndicator';
1213
import MessageInputWrapper from '../MessageInput';
13-
import { RenderCustomSeparatorProps, RenderMessageProps, TypingIndicatorType } from '../../../../types';
14+
import { RenderCustomSeparatorProps, RenderMessageParamsType, TypingIndicatorType } from '../../../../types';
1415

1516
export interface ChannelUIProps {
1617
isLoading?: boolean;
1718
renderPlaceholderLoader?: () => React.ReactElement;
1819
renderPlaceholderInvalid?: () => React.ReactElement;
1920
renderPlaceholderEmpty?: () => React.ReactElement;
2021
renderChannelHeader?: () => React.ReactElement;
21-
renderMessage?: (props: RenderMessageProps) => React.ReactElement;
22+
renderMessage?: (props: RenderMessageParamsType) => React.ReactElement;
23+
renderMessageContent?: (props: MessageContentProps) => React.ReactElement;
2224
renderMessageInput?: () => React.ReactElement;
2325
renderFileUploadIcon?: () => React.ReactElement;
2426
renderVoiceMessageIcon?: () => React.ReactElement;
@@ -35,6 +37,7 @@ const ChannelUI: React.FC<ChannelUIProps> = ({
3537
renderPlaceholderEmpty,
3638
renderChannelHeader,
3739
renderMessage,
40+
renderMessageContent,
3841
renderMessageInput,
3942
renderTypingIndicator,
4043
renderCustomSeparator,
@@ -108,6 +111,7 @@ const ChannelUI: React.FC<ChannelUIProps> = ({
108111
<MessageList
109112
className="sendbird-conversation__message-list"
110113
renderMessage={renderMessage}
114+
renderMessageContent={renderMessageContent}
111115
renderPlaceholderEmpty={renderPlaceholderEmpty}
112116
renderCustomSeparator={renderCustomSeparator}
113117
renderPlaceholderLoader={renderPlaceholderLoader}

src/modules/Channel/components/Message/index.tsx

Lines changed: 49 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -22,40 +22,42 @@ import { MAX_USER_MENTION_COUNT, MAX_USER_SUGGESTION_COUNT } from '../../context
2222
import DateSeparator from '../../../../ui/DateSeparator';
2323
import Label, { LabelTypography, LabelColors } from '../../../../ui/Label';
2424
import MessageInput from '../../../../ui/MessageInput';
25-
import MessageContent from '../../../../ui/MessageContent';
25+
import MessageContent, { type MessageContentProps } from '../../../../ui/MessageContent';
2626
import FileViewer from '../FileViewer';
2727
import RemoveMessageModal from '../RemoveMessageModal';
2828
import { MessageInputKeys } from '../../../../ui/MessageInput/const';
29-
import { EveryMessage, RenderCustomSeparatorProps, RenderMessageProps } from '../../../../types';
29+
import { EveryMessage, RenderCustomSeparatorProps, RenderMessageParamsType } from '../../../../types';
3030
import { useLocalization } from '../../../../lib/LocalizationContext';
3131
import { useDirtyGetMentions } from '../../../Message/hooks/useDirtyGetMentions';
3232
import SuggestedReplies from '../SuggestedReplies';
33+
import { omitObjectProperties } from '../../../../utils/omitObjectProperty';
3334

34-
type MessageUIProps = {
35+
export interface MessageUIProps {
3536
message: EveryMessage;
3637
hasSeparator?: boolean;
3738
chainTop?: boolean;
3839
chainBottom?: boolean;
3940
handleScroll?: (isBottomMessageAffected?: boolean) => void;
4041
// for extending
41-
renderMessage?: (props: RenderMessageProps) => React.ReactElement;
42+
renderMessage?: (props: RenderMessageParamsType) => React.ReactElement;
43+
renderMessageContent?: (props: MessageContentProps) => React.ReactElement;
4244
renderCustomSeparator?: (props: RenderCustomSeparatorProps) => React.ReactElement;
4345
renderEditInput?: () => React.ReactElement;
44-
renderMessageContent?: () => React.ReactElement;
45-
};
46+
}
4647

4748
// todo: Refactor this component, is too complex now
48-
const Message = ({
49-
message,
50-
hasSeparator,
51-
chainTop,
52-
chainBottom,
53-
handleScroll,
54-
renderCustomSeparator,
55-
renderEditInput,
56-
renderMessage,
57-
renderMessageContent,
58-
}: MessageUIProps): React.ReactElement => {
49+
const Message = (props: MessageUIProps): React.ReactElement => {
50+
const {
51+
message,
52+
hasSeparator,
53+
chainTop,
54+
chainBottom,
55+
handleScroll,
56+
renderCustomSeparator,
57+
renderEditInput,
58+
renderMessage,
59+
renderMessageContent = (props) => (<MessageContent {...props} />),
60+
} = props;
5961
const { dateLocale, stringSet } = useLocalization();
6062
const globalStore = useSendbirdStateContext();
6163

@@ -69,6 +71,7 @@ const Message = ({
6971
const maxUserMentionCount = userMention?.maxMentionCount || MAX_USER_MENTION_COUNT;
7072
const maxUserSuggestionCount = userMention?.maxSuggestionCount || MAX_USER_SUGGESTION_COUNT;
7173

74+
const context = useChannelContext();
7275
const {
7376
initialized,
7477
currentGroupChannel,
@@ -94,7 +97,7 @@ const Message = ({
9497
onMessageHighlighted,
9598
sendMessage,
9699
localMessages,
97-
} = useChannelContext();
100+
} = context;
98101
const [showEdit, setShowEdit] = useState(false);
99102
const [showRemove, setShowRemove] = useState(false);
100103
const [showFileViewer, setShowFileViewer] = useState(false);
@@ -191,20 +194,10 @@ const Message = ({
191194
clearTimeout(messageAnimatedTimeout);
192195
};
193196
}, [animatedMessageId, messageScrollRef.current, message.messageId, onMessageAnimated]);
194-
const renderedMessage = useMemo(() => {
195-
return renderMessage?.({
196-
message,
197-
chainTop,
198-
chainBottom,
199-
});
200-
}, [message, renderMessage]);
201-
const renderedCustomSeparator = useMemo(() => {
202-
if (renderCustomSeparator) {
203-
return renderCustomSeparator?.({ message: message });
204-
}
205-
return null;
206-
}, [message, renderCustomSeparator]);
207197

198+
// Operate `renderMessage` props
199+
const renderedCustomSeparator = useMemo(() => renderCustomSeparator?.({ message: message }) ?? null, [message, renderCustomSeparator]);
200+
const renderedMessage = useMemo(() => renderMessage?.(omitObjectProperties(props, ['renderMessage'])), [message, renderMessage]);
208201
if (renderedMessage) {
209202
return (
210203
<div
@@ -346,35 +339,31 @@ const Message = ({
346339
))
347340
}
348341
{/* Message */}
349-
{
350-
renderMessageContent?.() || (
351-
<MessageContent
352-
className="sendbird-message-hoc__message-content"
353-
userId={userId}
354-
scrollToMessage={scrollToMessage}
355-
channel={currentGroupChannel}
356-
message={message}
357-
disabled={!isOnline}
358-
chainTop={chainTop}
359-
chainBottom={chainBottom}
360-
isReactionEnabled={isReactionEnabled}
361-
replyType={replyType}
362-
threadReplySelectType={threadReplySelectType}
363-
nicknamesMap={nicknamesMap}
364-
emojiContainer={emojiContainer}
365-
showEdit={setShowEdit}
366-
showRemove={setShowRemove}
367-
showFileViewer={setShowFileViewer}
368-
resendMessage={resendMessage}
369-
deleteMessage={deleteMessage}
370-
toggleReaction={toggleReaction}
371-
setQuoteMessage={setQuoteMessage}
372-
onReplyInThread={onReplyInThread}
373-
onQuoteMessageClick={onQuoteMessageClick}
374-
onMessageHeightChange={handleScroll}
375-
/>
376-
)
377-
}
342+
{renderMessageContent({
343+
className: 'sendbird-message-hoc__message-content',
344+
userId,
345+
scrollToMessage,
346+
channel: currentGroupChannel,
347+
message,
348+
disabled: !isOnline,
349+
chainTop,
350+
chainBottom,
351+
isReactionEnabled,
352+
replyType,
353+
threadReplySelectType,
354+
nicknamesMap,
355+
emojiContainer,
356+
showEdit: setShowEdit,
357+
showRemove: setShowRemove,
358+
showFileViewer: setShowFileViewer,
359+
resendMessage,
360+
deleteMessage,
361+
toggleReaction,
362+
setQuoteMessage,
363+
onReplyInThread,
364+
onQuoteMessageClick: onQuoteMessageClick,
365+
onMessageHeightChange: handleScroll,
366+
})}
378367
{/** Suggested Replies */}
379368
{message.messageId === currentGroupChannel?.lastMessage?.messageId
380369
// the options should appear only when there's no failed or pending messages

src/modules/Channel/components/MessageList/index.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
import './message-list.scss';
22

33
import React, { useState } from 'react';
4+
import type { UserMessage } from '@sendbird/chat/message';
45

6+
import type { MessageContentProps } from '../../../../ui/MessageContent';
57
import { useChannelContext } from '../../context/ChannelProvider';
68
import PlaceHolder, { PlaceHolderTypes } from '../../../../ui/PlaceHolder';
79
import Icon, { IconColors, IconTypes } from '../../../../ui/Icon';
810
import Message from '../Message';
9-
import { EveryMessage, RenderCustomSeparatorProps, RenderMessageProps, TypingIndicatorType } from '../../../../types';
11+
import { EveryMessage, RenderCustomSeparatorProps, RenderMessageParamsType, TypingIndicatorType } from '../../../../types';
1012
import { isAboutSame } from '../../context/utils';
1113
import { getMessagePartsInfo } from './getMessagePartsInfo';
1214
import UnreadCount from '../UnreadCount';
1315
import FrozenNotification from '../FrozenNotification';
1416
import { SCROLL_BUFFER } from '../../../../utils/consts';
1517
import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext';
16-
import { UserMessage } from '@sendbird/chat/message';
1718
import { MessageProvider } from '../../../Message/context/MessageProvider';
1819
import { useHandleOnScrollCallback } from '../../../../hooks/useHandleOnScrollCallback';
1920
import { useSetScrollToBottom } from './hooks/useSetScrollToBottom';
@@ -25,7 +26,8 @@ const SCROLL_BOTTOM_PADDING = 50;
2526

2627
export interface MessageListProps {
2728
className?: string;
28-
renderMessage?: (props: RenderMessageProps) => React.ReactElement;
29+
renderMessage?: (props: RenderMessageParamsType) => React.ReactElement;
30+
renderMessageContent?: (props: MessageContentProps) => React.ReactElement;
2931
renderPlaceholderEmpty?: () => React.ReactElement;
3032
renderCustomSeparator?: (props: RenderCustomSeparatorProps) => React.ReactElement;
3133
renderPlaceholderLoader?: () => React.ReactElement;
@@ -35,6 +37,7 @@ export interface MessageListProps {
3537
const MessageList: React.FC<MessageListProps> = ({
3638
className = '',
3739
renderMessage,
40+
renderMessageContent,
3841
renderPlaceholderEmpty,
3942
renderCustomSeparator,
4043
renderPlaceholderLoader,
@@ -217,6 +220,7 @@ const MessageList: React.FC<MessageListProps> = ({
217220
<Message
218221
handleScroll={moveScroll}
219222
renderMessage={renderMessage}
223+
renderMessageContent={renderMessageContent}
220224
message={m as EveryMessage}
221225
hasSeparator={hasSeparator}
222226
chainTop={chainTop}
@@ -246,6 +250,7 @@ const MessageList: React.FC<MessageListProps> = ({
246250
<Message
247251
handleScroll={moveScroll}
248252
renderMessage={renderMessage}
253+
renderMessageContent={renderMessageContent}
249254
message={m as EveryMessage}
250255
chainTop={chainTop}
251256
chainBottom={chainBottom}

src/modules/Channel/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const Channel: React.FC<ChannelProps> = (props: ChannelProps) => {
4949
renderPlaceholderEmpty={props?.renderPlaceholderEmpty}
5050
renderChannelHeader={props?.renderChannelHeader}
5151
renderMessage={props?.renderMessage}
52+
renderMessageContent={props?.renderMessageContent}
5253
renderMessageInput={props?.renderMessageInput}
5354
renderTypingIndicator={props?.renderTypingIndicator}
5455
renderCustomSeparator={props?.renderCustomSeparator}

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
UserMessage,
99
} from '@sendbird/chat/message';
1010
import { CoreMessageType } from './utils';
11+
import { MessageUIProps } from './modules/Channel/components/Message';
1112

1213
export type ReplyType = 'NONE' | 'QUOTE_REPLY' | 'THREAD';
1314
export type Nullable<T> = T | null;
@@ -54,11 +55,14 @@ export interface ClientMessage {
5455
_sender: User;
5556
}
5657

58+
// This is used for OpenChannel.renderMessage
5759
export interface RenderMessageProps {
5860
message: CoreMessageType;
5961
chainTop: boolean;
6062
chainBottom: boolean;
6163
}
64+
// This is used for GroupChannel.renderMessage
65+
export type RenderMessageParamsType = Omit<MessageUIProps, 'renderMessage'>;
6266

6367
export interface RenderCustomSeparatorProps {
6468
message: CoreMessageType;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { omitObjectProperties } from '../omitObjectProperty';
2+
3+
const mockObject = {
4+
a: 'a',
5+
b: 'b',
6+
c: 'c',
7+
one: 1,
8+
two: 2,
9+
null: null,
10+
undefined: undefined,
11+
};
12+
13+
describe('Global-utils/omitObjectProperties', () => {
14+
it('should omit the existing properties from object', () => {
15+
expect(omitObjectProperties(mockObject, ['a', 'two', 'null', 'undefined']))
16+
.toEqual({
17+
b: 'b',
18+
c: 'c',
19+
one: 1,
20+
});
21+
});
22+
23+
it('should not omit not-existing properties', () => {
24+
expect(omitObjectProperties(mockObject, ['d', 'three', 'NaN']))
25+
.toEqual(mockObject);
26+
});
27+
28+
it('should not affect to the original object', () => {
29+
const clone = { ...mockObject };
30+
expect(omitObjectProperties(mockObject, ['a', 'two', 'null', 'undefined']))
31+
.toEqual({
32+
b: 'b',
33+
c: 'c',
34+
one: 1,
35+
});
36+
expect(mockObject).toEqual(clone);
37+
});
38+
});

src/utils/omitObjectProperty.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function omitObjectProperties<O extends Record<string, any>>(obj: O, properties: string[]) {
2+
properties.forEach((propertyName) => {
3+
if (Object.hasOwn(obj, propertyName)) delete obj[propertyName];
4+
});
5+
return obj;
6+
}

0 commit comments

Comments
 (0)