Skip to content

Commit 8ca1f97

Browse files
authored
fix: message render structure (#988)
- Resolved an infinite loop issue occurring when using the `GroupChannel/components/Message`, `Channel/components/Message` in the `renderMessage` method of the `GroupChannel`,`Channel` module. - The `renderMessage` method of the `GroupChannel` module no longer nests messages under the `Message` component. If a container element for the `Message` component is needed, use it as follows: ```tsx import { GroupChannel } from '@sendbird/uikit-react/GroupChannel'; import { Message } from '@sendbird/uikit-react/GroupChannel/components/Message'; const GroupChannelPage = () => { return ( <GroupChannel renderMessage={(props) => { return ( <Message message={props.message}> <div>{props.message.messageId}</div> </Message> ) }} /> ) }) ``` - The `renderMessage` prop of the `Channel/components/Message` and `GroupChannel/components/Message` components has been deprecated. Instead, use `children` prop to customize message sub-elements. ```tsx <Message message={props.message}> <div>{props.message.messageId}</div> </Message> ``` - Added detailed comments for customizing-related props in `GroupChannel` module.
1 parent 6e2fe92 commit 8ca1f97

File tree

7 files changed

+241
-211
lines changed

7 files changed

+241
-211
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import MessageInputWrapper from '../MessageInputWrapper';
99

1010
export interface ChannelUIProps extends GroupChannelUIBasicProps {
1111
isLoading?: boolean;
12+
/**
13+
* Customizes all child components of the message component.
14+
* */
15+
renderMessage?: GroupChannelUIBasicProps['renderMessage'];
1216
}
1317

1418
const ChannelUI = (props: ChannelUIProps) => {

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import { getSuggestedReplies } from '../../../../utils';
44
import MessageInputWrapperView from '../../../GroupChannel/components/MessageInputWrapper/MessageInputWrapperView';
55
import { useChannelContext } from '../../context/ChannelProvider';
66
import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext';
7+
import { GroupChannelUIBasicProps } from '../../../GroupChannel/components/GroupChannelUI/GroupChannelUIView';
78

89
export interface MessageInputWrapperProps {
910
value?: string;
1011
disabled?: boolean;
11-
renderFileUploadIcon?: () => React.ReactElement;
12-
renderVoiceMessageIcon?: () => React.ReactElement;
13-
renderSendMessageIcon?: () => React.ReactElement;
1412
acceptableMimeTypes?: string[];
13+
renderFileUploadIcon?: GroupChannelUIBasicProps['renderFileUploadIcon'];
14+
renderVoiceMessageIcon?: GroupChannelUIBasicProps['renderVoiceMessageIcon'];
15+
renderSendMessageIcon?: GroupChannelUIBasicProps['renderSendMessageIcon'];
1516
}
1617

1718
export const MessageInputWrapper = (props: MessageInputWrapperProps) => {

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

Lines changed: 75 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,24 @@ import { useOnScrollPositionChangeDetector } from '../../../../hooks/useOnScroll
2323

2424
import { getMessagePartsInfo } from '../../../GroupChannel/components/MessageList/getMessagePartsInfo';
2525
import { GroupChannelMessageListProps } from '../../../GroupChannel/components/MessageList';
26+
import { GroupChannelUIBasicProps } from '../../../GroupChannel/components/GroupChannelUI/GroupChannelUIView';
2627

2728
const SCROLL_BOTTOM_PADDING = 50;
2829

29-
export type MessageListProps = GroupChannelMessageListProps;
30+
export interface MessageListProps extends GroupChannelMessageListProps {
31+
/**
32+
* Customizes all child components of the message component.
33+
* */
34+
renderMessage?: GroupChannelUIBasicProps['renderMessage'];
35+
}
3036
export const MessageList = ({
3137
className = '',
3238
renderMessage,
3339
renderMessageContent,
34-
renderPlaceholderEmpty,
3540
renderCustomSeparator,
36-
renderPlaceholderLoader,
37-
renderFrozenNotification,
41+
renderPlaceholderLoader = () => <PlaceHolder type={PlaceHolderTypes.LOADING} />,
42+
renderPlaceholderEmpty = () => <PlaceHolder className="sendbird-conversation__no-messages" type={PlaceHolderTypes.NO_MESSAGES} />,
43+
renderFrozenNotification = () => <FrozenNotification className="sendbird-conversation__messages__notification" />,
3844
}: MessageListProps) => {
3945
const {
4046
allMessages,
@@ -62,9 +68,7 @@ export const MessageList = ({
6268
} = useChannelContext();
6369

6470
const store = useSendbirdStateContext();
65-
const allMessagesFiltered = (typeof filterMessageList === 'function')
66-
? allMessages.filter((filterMessageList as (message: EveryMessage) => boolean))
67-
: allMessages;
71+
const allMessagesFiltered = typeof filterMessageList === 'function' ? allMessages.filter(filterMessageList as (message: EveryMessage) => boolean) : allMessages;
6872
const markAsReadScheduler = store.config.markAsReadScheduler;
6973

7074
const [isScrollBottom, setIsScrollBottom] = useState(false);
@@ -80,11 +84,7 @@ export const MessageList = ({
8084
return;
8185
}
8286

83-
const {
84-
scrollTop,
85-
clientHeight,
86-
scrollHeight,
87-
} = element;
87+
const { scrollTop, clientHeight, scrollHeight } = element;
8888

8989
if (hasMorePrev && isAboutSame(scrollTop, 0, SCROLL_BUFFER)) {
9090
onScrollCallback(callback);
@@ -94,10 +94,7 @@ export const MessageList = ({
9494
onScrollDownCallback(callback);
9595
}
9696

97-
if (!disableMarkAsRead
98-
&& isAboutSame(clientHeight + scrollTop, scrollHeight, SCROLL_BUFFER)
99-
&& !!currentGroupChannel
100-
) {
97+
if (!disableMarkAsRead && isAboutSame(clientHeight + scrollTop, scrollHeight, SCROLL_BUFFER) && !!currentGroupChannel) {
10198
messagesDispatcher({
10299
type: messageActionTypes.MARK_AS_READ,
103100
payload: { channel: currentGroupChannel },
@@ -124,8 +121,7 @@ export const MessageList = ({
124121
const current = scrollRef?.current;
125122
if (current) {
126123
const bottom = current.scrollHeight - current.scrollTop - current.offsetHeight;
127-
if (scrollBottom < bottom
128-
&& (!isBottomMessageAffected || scrollBottom < SCROLL_BUFFER)) {
124+
if (scrollBottom < bottom && (!isBottomMessageAffected || scrollBottom < SCROLL_BUFFER)) {
129125
// Move the scroll as much as the height of the message has changed
130126
current.scrollTop += bottom - scrollBottom;
131127
}
@@ -165,22 +161,16 @@ export const MessageList = ({
165161
const { scrollToBottomHandler, scrollBottom } = useSetScrollToBottom({ loading });
166162

167163
if (loading) {
168-
return (typeof renderPlaceholderLoader === 'function')
169-
? renderPlaceholderLoader()
170-
: <PlaceHolder type={PlaceHolderTypes.LOADING} />;
164+
return renderPlaceholderLoader();
171165
}
166+
172167
if (allMessagesFiltered.length < 1) {
173-
if (renderPlaceholderEmpty && typeof renderPlaceholderEmpty === 'function') {
174-
return renderPlaceholderEmpty();
175-
}
176-
return <PlaceHolder className="sendbird-conversation__no-messages" type={PlaceHolderTypes.NO_MESSAGES} />;
168+
return renderPlaceholderEmpty();
177169
}
178170

179171
return (
180172
<>
181-
{
182-
!isScrolled && <PlaceHolder type={PlaceHolderTypes.LOADING} />
183-
}
173+
{!isScrolled && <PlaceHolder type={PlaceHolderTypes.LOADING} />}
184174
<div className={`sendbird-conversation__messages ${className}`}>
185175
<div className="sendbird-conversation__scroll-container">
186176
<div className="sendbird-conversation__padding" />
@@ -193,86 +183,67 @@ export const MessageList = ({
193183
onScrollReachedEndDetector(e);
194184
}}
195185
>
196-
{
197-
allMessagesFiltered.map((m, idx) => {
198-
const {
199-
chainTop,
200-
chainBottom,
201-
hasSeparator,
202-
} = getMessagePartsInfo({
203-
allMessages: allMessagesFiltered,
204-
replyType,
205-
isMessageGroupingEnabled,
206-
currentIndex: idx,
207-
currentMessage: m,
208-
currentChannel: currentGroupChannel,
209-
});
210-
const isByMe = (m as UserMessage)?.sender?.userId === store?.config?.userId;
211-
return (
212-
<MessageProvider message={m} key={m?.messageId} isByMe={isByMe}>
213-
<Message
214-
handleScroll={moveScroll}
215-
renderMessage={renderMessage}
216-
renderMessageContent={renderMessageContent}
217-
message={m as EveryMessage}
218-
hasSeparator={hasSeparator}
219-
chainTop={chainTop}
220-
chainBottom={chainBottom}
221-
renderCustomSeparator={renderCustomSeparator}
222-
/>
223-
</MessageProvider>
224-
);
225-
})
226-
}
227-
{
228-
localMessages.map((m, idx) => {
229-
const {
230-
chainTop,
231-
chainBottom,
232-
} = getMessagePartsInfo({
233-
allMessages: allMessagesFiltered,
234-
replyType,
235-
isMessageGroupingEnabled,
236-
currentIndex: idx,
237-
currentMessage: m,
238-
currentChannel: currentGroupChannel,
239-
});
240-
const isByMe = (m as UserMessage)?.sender?.userId === store?.config?.userId;
241-
return (
242-
<MessageProvider message={m} key={m?.messageId} isByMe={isByMe}>
243-
<Message
244-
handleScroll={moveScroll}
245-
renderMessage={renderMessage}
246-
renderMessageContent={renderMessageContent}
247-
message={m as EveryMessage}
248-
chainTop={chainTop}
249-
chainBottom={chainBottom}
250-
renderCustomSeparator={renderCustomSeparator}
251-
/>
252-
</MessageProvider>
253-
);
254-
})
255-
}
256-
{
257-
!hasMoreNext
186+
{allMessagesFiltered.map((m, idx) => {
187+
const { chainTop, chainBottom, hasSeparator } = getMessagePartsInfo({
188+
allMessages: allMessagesFiltered,
189+
replyType,
190+
isMessageGroupingEnabled,
191+
currentIndex: idx,
192+
currentMessage: m,
193+
currentChannel: currentGroupChannel,
194+
});
195+
const isByMe = (m as UserMessage)?.sender?.userId === store?.config?.userId;
196+
return (
197+
<MessageProvider message={m} key={m?.messageId} isByMe={isByMe}>
198+
<Message
199+
handleScroll={moveScroll}
200+
message={m as EveryMessage}
201+
hasSeparator={hasSeparator}
202+
chainTop={chainTop}
203+
chainBottom={chainBottom}
204+
renderMessageContent={renderMessageContent}
205+
renderCustomSeparator={renderCustomSeparator}
206+
// backward compatibility
207+
renderMessage={renderMessage}
208+
/>
209+
</MessageProvider>
210+
);
211+
})}
212+
{localMessages.map((m, idx) => {
213+
const { chainTop, chainBottom } = getMessagePartsInfo({
214+
allMessages: allMessagesFiltered,
215+
replyType,
216+
isMessageGroupingEnabled,
217+
currentIndex: idx,
218+
currentMessage: m,
219+
currentChannel: currentGroupChannel,
220+
});
221+
const isByMe = (m as UserMessage)?.sender?.userId === store?.config?.userId;
222+
return (
223+
<MessageProvider message={m} key={m?.messageId} isByMe={isByMe}>
224+
<Message
225+
handleScroll={moveScroll}
226+
message={m as EveryMessage}
227+
chainTop={chainTop}
228+
chainBottom={chainBottom}
229+
renderMessageContent={renderMessageContent}
230+
renderCustomSeparator={renderCustomSeparator}
231+
// backward compatibility
232+
renderMessage={renderMessage}
233+
/>
234+
</MessageProvider>
235+
);
236+
})}
237+
{!hasMoreNext
258238
&& store?.config?.groupChannel?.enableTypingIndicator
259-
&& store?.config?.groupChannel?.typingIndicatorTypes?.has(TypingIndicatorType.Bubble)
260-
&& <TypingIndicatorBubble
261-
typingMembers={typingMembers}
262-
handleScroll={moveScroll}
263-
/>
264-
}
239+
&& store?.config?.groupChannel?.typingIndicatorTypes?.has(TypingIndicatorType.Bubble) && (
240+
<TypingIndicatorBubble typingMembers={typingMembers} handleScroll={moveScroll} />
241+
)}
265242
{/* show frozen notifications, */}
266243
{/* show new message notifications, */}
267244
</div>
268245
</div>
269-
{
270-
currentGroupChannel?.isFrozen && (
271-
renderFrozenNotification
272-
? renderFrozenNotification()
273-
: <FrozenNotification className="sendbird-conversation__messages__notification" />
274-
)
275-
}
246+
{currentGroupChannel?.isFrozen && renderFrozenNotification()}
276247
{
277248
/**
278249
* Show unread count IFF scroll is not bottom or is bottom but hasNext is true.
@@ -309,12 +280,7 @@ export const MessageList = ({
309280
tabIndex={0}
310281
role="button"
311282
>
312-
<Icon
313-
width="24px"
314-
height="24px"
315-
type={IconTypes.CHEVRON_DOWN}
316-
fillColor={IconColors.PRIMARY}
317-
/>
283+
<Icon width="24px" height="24px" type={IconTypes.CHEVRON_DOWN} fillColor={IconColors.PRIMARY} />
318284
</div>
319285
)
320286
}

src/modules/GroupChannel/components/GroupChannelUI/GroupChannelUIView.tsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,66 @@ import type { MessageContentProps } from '../../../../ui/MessageContent';
1515

1616
export interface GroupChannelUIBasicProps {
1717
// Components
18+
/**
19+
* A function that customizes the rendering of a loading placeholder component.
20+
*/
1821
renderPlaceholderLoader?: () => React.ReactElement;
22+
/**
23+
* A function that customizes the rendering of a invalid placeholder component.
24+
*/
1925
renderPlaceholderInvalid?: () => React.ReactElement;
26+
/**
27+
* A function that customizes the rendering of an empty placeholder component when there are no messages in the channel.
28+
*/
2029
renderPlaceholderEmpty?: () => React.ReactElement;
30+
/**
31+
* A function that customizes the rendering of a header component.
32+
*/
2133
renderChannelHeader?: (props: GroupChannelHeaderProps) => React.ReactElement;
34+
/**
35+
* A function that customizes the rendering of a message list component.
36+
*/
2237
renderMessageList?: (props: GroupChannelMessageListProps) => React.ReactElement;
38+
/**
39+
* A function that customizes the rendering of a message input component.
40+
*/
2341
renderMessageInput?: () => React.ReactElement;
2442

2543
// Sub components
2644
// MessageList
45+
/**
46+
* A function that customizes the rendering of each message component in the message list component.
47+
*/
2748
renderMessage?: (props: RenderMessageParamsType) => React.ReactElement;
49+
/**
50+
* A function that customizes the rendering of the content portion of each message component.
51+
*/
2852
renderMessageContent?: (props: MessageContentProps) => React.ReactElement;
53+
/**
54+
* A function that customizes the rendering of a separator component between messages.
55+
*/
2956
renderCustomSeparator?: (props: RenderCustomSeparatorProps) => React.ReactElement;
57+
/**
58+
* A function that customizes the rendering of a frozen notification component when the channel is frozen.
59+
*/
3060
renderFrozenNotification?: () => React.ReactElement;
3161

32-
// MessageInput
62+
// MessageInput
63+
/**
64+
* A function that customizes the rendering of the file upload icon in the message input component.
65+
*/
3366
renderFileUploadIcon?: () => React.ReactElement;
67+
/**
68+
* A function that customizes the rendering of the voice message icon in the message input component.
69+
*/
3470
renderVoiceMessageIcon?: () => React.ReactElement;
71+
/**
72+
* A function that customizes the rendering of the send message icon in the message input component.
73+
*/
3574
renderSendMessageIcon?: () => React.ReactElement;
75+
/**
76+
* A function that customizes the rendering of the typing indicator component.
77+
*/
3678
renderTypingIndicator?: () => React.ReactElement;
3779
}
3880

0 commit comments

Comments
 (0)