Skip to content

Commit 6fb1baa

Browse files
chohongmbang9
andauthored
feat: Improve message template view (#1207)
Fixes [CLNP-4852](https://sendbird.atlassian.net/browse/CLNP-4852) ### Changelogs - Will be added soon. ### How to test? 그룹채널에 유저 두명: test_liam_1, test_liam_2 (닉네임도 유저id와 동일 함) 로컬 url: http://localhost:5173/group_channel?appId=5D27A98C-D935-4EDA-846A-BCCD90E8E55B&userId=test_liam_1&nickname=test_liam_1&accessToken=3810634cd02e61956f08edf4fd614bfbe9e3e505 대시보드: https://dashboard.sendbird.com/5D27A98C-D935-4EDA-846A-BCCD90E8E55B/settings/general ### Figma https://www.figma.com/design/SVbXU00FhjztekD8AiVukK/WIP_UIKit_React?node-id=2924-4031&m=dev <img width="1012" alt="Screenshot 2024-09-06 at 2 13 32 PM" src="https://github.com/user-attachments/assets/c703765c-db6f-4726-b721-efe3f8e0c6e1"> ### Default container type rendering (template message only) [WIP_UIKit_React – Figma.webm](https://github.com/user-attachments/assets/04c65a8f-6f0d-4ac9-91b9-d19821eb4d03) [CLNP-4852]: https://sendbird.atlassian.net/browse/CLNP-4852?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --------- Co-authored-by: Hyungu Kang | Airen <[email protected]>
1 parent 4922d04 commit 6fb1baa

File tree

28 files changed

+526
-487
lines changed

28 files changed

+526
-487
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@
7070
},
7171
"dependencies": {
7272
"@sendbird/chat": "^4.14.2",
73-
"@sendbird/react-uikit-message-template-view": "0.0.1-alpha.79",
74-
"@sendbird/uikit-tools": "0.0.1-alpha.79",
73+
"@sendbird/react-uikit-message-template-view": "0.0.2",
74+
"@sendbird/uikit-tools": "0.0.2",
7575
"css-vars-ponyfill": "^2.3.2",
7676
"date-fns": "^2.16.1",
7777
"dompurify": "^3.0.1"
Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1-
import React, { useLayoutEffect, useState } from 'react';
1+
import React, { useCallback, useEffect, useState } from 'react';
22

3-
export const useIsElementInViewport = (elementRef: React.MutableRefObject<any>) => {
3+
export const useIsElementInViewport = (): [React.RefCallback<HTMLDivElement>, boolean] => {
44
const [isVisible, setIsVisible] = useState(false);
5+
const [element, setElement] = useState(null);
56

6-
useLayoutEffect(() => {
7-
const observer = new IntersectionObserver((entries: IntersectionObserverEntry[]) => {
7+
const ref = useCallback((node) => {
8+
if (node !== null) setElement(node);
9+
}, []);
10+
11+
useEffect(() => {
12+
if (!element) return;
13+
14+
const observer = new IntersectionObserver((entries) => {
815
const [entry] = entries;
9-
if (entry) setIsVisible(entry.isIntersecting);
16+
setIsVisible(entry.isIntersecting);
1017
});
1118

12-
if (elementRef.current) observer.observe(elementRef.current);
13-
return () => observer.disconnect();
14-
}, [elementRef.current]);
19+
observer.observe(element);
20+
21+
return () => {
22+
observer.disconnect();
23+
};
24+
}, [element]);
1525

16-
return isVisible;
26+
return [ref, isVisible];
1727
};
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import React, { useRef } from 'react';
1+
import { RefCallback, useRef } from 'react';
22
import { useIsElementInViewport } from './useIsElementInViewport';
33

4-
export const useLazyImageLoader = (elementRef: React.MutableRefObject<any>) => {
4+
export const useLazyImageLoader = (): [RefCallback<HTMLDivElement>, boolean] => {
55
const isLoaded = useRef(false);
6-
const isVisible = useIsElementInViewport(elementRef);
6+
const [ref, isVisible] = useIsElementInViewport();
77

88
if (isVisible) isLoaded.current = true;
9-
return isLoaded.current;
9+
return [ref, isLoaded.current];
1010
};

src/types.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,4 @@ export interface UploadedFileInfoWithUpload {
8787

8888
export type SendbirdTheme = 'light' | 'dark';
8989

90-
export enum MessageContentMiddleContainerType {
91-
DEFAULT = 'default',
92-
WIDE = 'wide',
93-
}
94-
9590
export type HTMLTextDirection = 'ltr' | 'rtl';

src/ui/ChannelAvatar/__tests__/ChannelAvatar.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('ui/ChannelAvatar', () => {
1717
expect(container.getElementsByClassName(targetClassName)[0].className).toContain(targetClassName);
1818
});
1919

20-
it('should render an avatar broadcastChannel with url', function() {
20+
it('should render an avatar broadcastChannel with non default url', function() {
2121
const targetClassName = "sendbird-chat-header--avatar--broadcast-channel";
2222
const coverUrl = '123';
2323
render(<ChannelAvatar channel={{ isBroadcast: true, coverUrl }} />);

src/ui/ContextMenu/index.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
display: inline;
55
}
66

7+
.sendbird-message-content__sendbird-ui-container-type__default__header-container .sendbird-context-menu {
8+
display: flex;
9+
}
10+
711
.sendbird__offline {
812
.sendbird-dropdown__menu .sendbird-dropdown__menu-item {
913
cursor: not-allowed;

src/ui/ContextMenu/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ export default function ContextMenu({
8484
return (
8585
<div
8686
className="sendbird-context-menu"
87-
style={{ display: 'inline' }}
8887
onClick={onClick}
8988
>
9089
{menuTrigger?.(() => setShowMenu(!showMenu))}

src/ui/ImageRenderer/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,8 @@ const ImageRenderer = ({
6161
shadeOnHover,
6262
isUploaded = true,
6363
}: ImageRendererProps): ReactElement => {
64-
const ref = useRef(null);
65-
const isLoaded = useLazyImageLoader(ref);
66-
const internalUrl = isLoaded ? url : null;
64+
const [ref, isVisible] = useLazyImageLoader();
65+
const internalUrl = isVisible ? url : null;
6766

6867
const [defaultComponentVisible, setDefaultComponentVisible] = useState(false);
6968
const [placeholderVisible, setPlaceholderVisible] = useState(true);

src/ui/MessageContent/MessageBody/index.tsx

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
CoreMessageType,
55
getUIKitMessageType, getUIKitMessageTypes, isTemplateMessage, isMultipleFilesMessage,
66
isOGMessage, isSendableMessage,
7-
isTextMessage, isThumbnailMessage, isVoiceMessage, isFormMessage,
7+
isTextMessage, isThumbnailMessage, isVoiceMessage, isFormMessage, isValidTemplateMessageType,
88
} from '../../../utils';
99
import { BaseMessage, FileMessage, MultipleFilesMessage, UserMessage } from '@sendbird/chat/message';
1010
import OGMessageItemBody from '../../OGMessageItemBody';
@@ -23,21 +23,20 @@ import { match } from 'ts-pattern';
2323
import TemplateMessageItemBody from '../../TemplateMessageItemBody';
2424
import type { OnBeforeDownloadFileMessageType } from '../../../modules/GroupChannel/context/GroupChannelProvider';
2525
import FormMessageItemBody from '../../FormMessageItemBody';
26+
import { MESSAGE_TEMPLATE_KEY } from '../../../utils/consts';
2627

2728
export type CustomSubcomponentsProps = Record<
2829
'ThumbnailMessageItemBody' | 'MultipleFilesMessageItemBody',
2930
Record<string, any>
3031
>;
3132

3233
const MESSAGE_ITEM_BODY_CLASSNAME = 'sendbird-message-content__middle__message-item-body';
33-
export type RenderedTemplateBodyType = 'failed' | 'composite' | 'simple';
3434

3535
export interface MessageBodyProps {
3636
className?: string;
3737
channel: Nullable<GroupChannel>;
3838
message: CoreMessageType;
3939
showFileViewer?: (bool: boolean) => void;
40-
onTemplateMessageRenderedCallback?: (renderedTemplateBodyType: RenderedTemplateBodyType) => void;
4140
onMessageHeightChange?: () => void;
4241
onBeforeDownloadFileMessage?: OnBeforeDownloadFileMessageType;
4342

@@ -55,7 +54,6 @@ export const MessageBody = (props: MessageBodyProps) => {
5554
channel,
5655
showFileViewer,
5756
onMessageHeightChange,
58-
onTemplateMessageRenderedCallback,
5957
onBeforeDownloadFileMessage,
6058

6159
mouseHover,
@@ -76,6 +74,14 @@ export const MessageBody = (props: MessageBodyProps) => {
7674
const isOgMessageEnabledInGroupChannel = channel?.isGroupChannel() && config.groupChannel.enableOgtag;
7775
const isFormMessageEnabledInGroupChannel = channel?.isGroupChannel() && config.groupChannel.enableFormTypeMessage;
7876

77+
const renderUnknownMessageItemBody = () => <UnknownMessageItemBody
78+
className={className}
79+
message={message}
80+
isByMe={isByMe}
81+
mouseHover={mouseHover}
82+
isReactionEnabled={isReactionEnabledInChannel}
83+
/>;
84+
7985
return match(message)
8086
.when((message) => isFormMessageEnabledInGroupChannel && isFormMessage(message),
8187
() => (
@@ -85,15 +91,22 @@ export const MessageBody = (props: MessageBodyProps) => {
8591
form={message.messageForm}
8692
/>
8793
))
88-
.when(isTemplateMessage, () => (
89-
<TemplateMessageItemBody
94+
.when(isTemplateMessage, () => {
95+
const templatePayload = message.extendedMessagePayload[MESSAGE_TEMPLATE_KEY];
96+
if (!isValidTemplateMessageType(templatePayload)) {
97+
config.logger?.error?.(
98+
'TemplateMessageItemBody: invalid type value in message.extendedMessagePayload.message_template.',
99+
templatePayload,
100+
);
101+
return renderUnknownMessageItemBody();
102+
}
103+
return <TemplateMessageItemBody
90104
className={className}
91105
message={message as BaseMessage}
92106
isByMe={isByMe}
93107
theme={config?.theme as SendbirdTheme}
94-
onTemplateMessageRenderedCallback={onTemplateMessageRenderedCallback}
95-
/>
96-
))
108+
/>;
109+
})
97110
.when((message) => isOgMessageEnabledInGroupChannel
98111
&& isSendableMessage(message)
99112
&& isOGMessage(message), () => (
@@ -163,15 +176,9 @@ export const MessageBody = (props: MessageBodyProps) => {
163176
{...customSubcomponentsProps['ThumbnailMessageItemBody'] ?? {}}
164177
/>
165178
))
166-
.otherwise((message) => (
167-
<UnknownMessageItemBody
168-
className={className}
169-
message={message}
170-
isByMe={isByMe}
171-
mouseHover={mouseHover}
172-
isReactionEnabled={isReactionEnabledInChannel}
173-
/>
174-
));
179+
.otherwise(() => {
180+
return renderUnknownMessageItemBody();
181+
});
175182
};
176183

177184
export default MessageBody;
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import React, { ReactElement } from 'react';
2+
import Label, { LabelColors, LabelTypography } from '../Label';
3+
import { classnames } from '../../utils/utils';
4+
import format from 'date-fns/format';
5+
import { MessageTemplateData, TemplateType } from '../TemplateMessageItemBody/types';
6+
import { MessageComponentRenderers, MessageContentProps } from './index';
7+
import useSendbirdStateContext from '../../hooks/useSendbirdStateContext';
8+
import { uiContainerType } from '../../utils';
9+
import { useLocalization } from '../../lib/LocalizationContext';
10+
import { MESSAGE_TEMPLATE_KEY } from '../../utils/consts';
11+
12+
type MessageContentForTemplateMessageProps = MessageContentProps & MessageComponentRenderers & {
13+
isByMe: boolean;
14+
displayThreadReplies: boolean;
15+
mouseHover: boolean;
16+
isMobile: boolean;
17+
isReactionEnabledInChannel: boolean;
18+
hoveredMenuClassName: string;
19+
templateType: TemplateType | null;
20+
useReplying: boolean;
21+
};
22+
23+
export function MessageContentForTemplateMessage(props: MessageContentForTemplateMessageProps): ReactElement {
24+
const {
25+
channel,
26+
message,
27+
showFileViewer,
28+
onMessageHeightChange,
29+
onBeforeDownloadFileMessage,
30+
31+
renderSenderProfile,
32+
renderMessageHeader,
33+
renderMessageBody,
34+
35+
isByMe,
36+
displayThreadReplies,
37+
mouseHover,
38+
isMobile,
39+
isReactionEnabledInChannel,
40+
hoveredMenuClassName,
41+
templateType,
42+
useReplying,
43+
} = props;
44+
45+
const { config } = useSendbirdStateContext();
46+
const { dateLocale } = useLocalization();
47+
48+
const uiContainerTypeClassName = uiContainerType[templateType] ?? '';
49+
50+
const senderProfile = renderSenderProfile({
51+
...props,
52+
chainBottom: false,
53+
className: '',
54+
isByMe,
55+
displayThreadReplies,
56+
});
57+
const messageHeader = renderMessageHeader(props);
58+
const messageBody = renderMessageBody({
59+
message,
60+
channel,
61+
showFileViewer,
62+
onMessageHeightChange,
63+
mouseHover,
64+
isMobile,
65+
config,
66+
isReactionEnabledInChannel,
67+
isByMe,
68+
onBeforeDownloadFileMessage,
69+
});
70+
71+
const timeStamp = <Label
72+
className={classnames(
73+
'sendbird-message-content__middle__body-container__created-at',
74+
'right',
75+
hoveredMenuClassName,
76+
uiContainerTypeClassName,
77+
)}
78+
type={LabelTypography.CAPTION_3}
79+
color={LabelColors.ONBACKGROUND_2}
80+
>
81+
{format(message?.createdAt || 0, 'p', {
82+
locale: dateLocale,
83+
})}
84+
</Label>;
85+
86+
const templateData: MessageTemplateData = message.extendedMessagePayload?.[MESSAGE_TEMPLATE_KEY] as MessageTemplateData;
87+
const { profile = true, time = true, nickname = true } = templateData?.container_options ?? {};
88+
const hasContainerHeader = profile || nickname;
89+
90+
return (
91+
<div className="sendbird-message-content__sendbird-ui-container-type__default__root">
92+
{
93+
!isByMe
94+
&& hasContainerHeader
95+
&& !useReplying
96+
&& (
97+
<div className="sendbird-message-content__sendbird-ui-container-type__default__header-container">
98+
<div
99+
className="sendbird-message-content__sendbird-ui-container-type__default__header-container__left__profile">
100+
{profile && senderProfile}
101+
</div>
102+
{nickname && messageHeader}
103+
</div>
104+
)
105+
}
106+
{messageBody}
107+
{
108+
(!isByMe && time)
109+
&& <div className="sendbird-message-content__sendbird-ui-container-type__default__bottom">
110+
{timeStamp}
111+
</div>
112+
}
113+
</div>
114+
);
115+
}

0 commit comments

Comments
 (0)