From 022c0b206aa387da940fa7016d7dd353566ab6f8 Mon Sep 17 00:00:00 2001 From: HoonBaek Date: Mon, 26 Aug 2024 10:47:18 +0900 Subject: [PATCH 1/2] Add render~~ props to the ThreadListItemContent --- .../components/ThreadList/ThreadListItem.tsx | 1 - .../ThreadList/ThreadListItemContent.tsx | 324 +++++++----------- src/ui/MessageContent/MessageBody/index.tsx | 45 ++- .../MessageContent/MessageProfile/index.tsx | 36 +- src/ui/MessageContent/index.tsx | 6 +- 5 files changed, 170 insertions(+), 242 deletions(-) diff --git a/src/modules/Thread/components/ThreadList/ThreadListItem.tsx b/src/modules/Thread/components/ThreadList/ThreadListItem.tsx index a8e5cb96b..a271eb9e4 100644 --- a/src/modules/Thread/components/ThreadList/ThreadListItem.tsx +++ b/src/modules/Thread/components/ThreadList/ThreadListItem.tsx @@ -243,7 +243,6 @@ export default function ThreadListItem({ chainTop={chainTop} chainBottom={chainBottom} isReactionEnabled={isReactionEnabled} - isMentionEnabled={isMentionEnabled} disableQuoteMessage replyType={replyType} nicknamesMap={nicknamesMap} diff --git a/src/modules/Thread/components/ThreadList/ThreadListItemContent.tsx b/src/modules/Thread/components/ThreadList/ThreadListItemContent.tsx index d5d527259..1509686a0 100644 --- a/src/modules/Thread/components/ThreadList/ThreadListItemContent.tsx +++ b/src/modules/Thread/components/ThreadList/ThreadListItemContent.tsx @@ -1,61 +1,49 @@ -import React, { ReactNode, useRef, useState } from 'react'; +import React, { useRef, useState } from 'react'; import { EmojiContainer } from '@sendbird/chat'; -import { FileMessage, MultipleFilesMessage, UserMessage } from '@sendbird/chat/message'; +import { FileMessage, UserMessage } from '@sendbird/chat/message'; import { GroupChannel } from '@sendbird/chat/groupChannel'; import './ThreadListItemContent.scss'; import { ReplyType } from '../../../../types'; -import ContextMenu, { EMOJI_MENU_ROOT_ID, getObservingId, MENU_OBSERVING_CLASS_NAME, MENU_ROOT_ID, MenuItems } from '../../../../ui/ContextMenu'; -import Avatar from '../../../../ui/Avatar'; -import { useUserProfileContext } from '../../../../lib/UserProfileContext'; -import UserProfile from '../../../../ui/UserProfile'; -import { MessageEmojiMenu, MessageEmojiMenuProps } from '../../../../ui/MessageItemReactionMenu'; +import { EMOJI_MENU_ROOT_ID, getObservingId, MENU_OBSERVING_CLASS_NAME, MENU_ROOT_ID } from '../../../../ui/ContextMenu'; +import { MessageEmojiMenu } from '../../../../ui/MessageItemReactionMenu'; import Label, { LabelTypography, LabelColors } from '../../../../ui/Label'; import { getClassName, - getSenderName, - getUIKitMessageType, - getUIKitMessageTypes, isMultipleFilesMessage, isOGMessage, - isTextMessage, isThumbnailMessage, - isVoiceMessage, SendableMessageType, } from '../../../../utils'; import MessageStatus from '../../../../ui/MessageStatus'; -import EmojiReactions from '../../../../ui/EmojiReactions'; +import EmojiReactions, { EmojiReactionsProps } from '../../../../ui/EmojiReactions'; import format from 'date-fns/format'; import { useLocalization } from '../../../../lib/LocalizationContext'; -import TextMessageItemBody from '../../../../ui/TextMessageItemBody'; -import OGMessageItemBody from '../../../../ui/OGMessageItemBody'; -import FileMessageItemBody from '../../../../ui/FileMessageItemBody'; -import ThumbnailMessageItemBody from '../../../../ui/ThumbnailMessageItemBody'; -import UnknownMessageItemBody from '../../../../ui/UnknownMessageItemBody'; -import VoiceMessageItemBody from '../../../../ui/VoiceMessageItemBody'; import { useMediaQueryContext } from '../../../../lib/MediaQueryContext'; import useLongPress from '../../../../hooks/useLongPress'; import MobileMenu from '../../../../ui/MobileMenu'; import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext'; -import MultipleFilesMessageItemBody, { ThreadMessageKind } from '../../../../ui/MultipleFilesMessageItemBody'; +import { ThreadMessageKind } from '../../../../ui/MultipleFilesMessageItemBody'; import { useThreadMessageKindKeySelector } from '../../../Channel/context/hooks/useThreadMessageKindKeySelector'; import { useFileInfoListWithUploaded } from '../../../Channel/context/hooks/useFileInfoListWithUploaded'; import { useThreadContext } from '../../context/ThreadProvider'; -import { classnames } from '../../../../utils/utils'; +import { classnames, deleteNullish } from '../../../../utils/utils'; import { MessageMenu, MessageMenuProps } from '../../../../ui/MessageMenu'; import useElementObserver from '../../../../hooks/useElementObserver'; +import type { MessageContentRenderSubComponentProps } from '../../../../ui/MessageContent'; +import MessageProfile, { MessageProfileProps } from '../../../../ui/MessageContent/MessageProfile'; +import MessageBody, { CustomSubcomponentsProps, MessageBodyProps } from '../../../../ui/MessageContent/MessageBody'; +import { MessageHeaderProps, MessageHeader } from '../../../../ui/MessageContent/MessageHeader'; +import { MobileBottomSheetProps } from '../../../../ui/MobileMenu/types'; -export interface ThreadListItemContentProps { +export interface ThreadListItemContentProps extends MessageContentRenderSubComponentProps { className?: string; userId: string; channel: GroupChannel; message: SendableMessageType; - /** @deprecated This prop is deprecated and no longer in use. */ - disabled?: boolean; chainTop?: boolean; chainBottom?: boolean; - isMentionEnabled?: boolean; isReactionEnabled?: boolean; disableQuoteMessage?: boolean; replyType?: ReplyType; @@ -67,33 +55,48 @@ export interface ThreadListItemContentProps { resendMessage?: (message: SendableMessageType) => void; toggleReaction?: (message: SendableMessageType, reactionKey: string, isReacted: boolean) => void; onReplyInThread?: (props: { message: SendableMessageType }) => void; - renderEmojiMenu?: (props: MessageEmojiMenuProps) => ReactNode; - renderMessageMenu?: (props: MessageMenuProps) => ReactNode; + /** @deprecated This prop is deprecated and no longer in use. */ + disabled?: boolean; + /** @deprecated This props is deprecated and no longer in use. */ + isMentionEnabled?: boolean; } -export default function ThreadListItemContent({ - className, - userId, - channel, - message, - chainTop = false, - chainBottom = false, - isMentionEnabled = false, - isReactionEnabled = false, - disableQuoteMessage = false, - replyType, - nicknamesMap, - emojiContainer, - showEdit, - showRemove, - showFileViewer, - resendMessage, - toggleReaction, - onReplyInThread, - renderEmojiMenu = (props) => , - renderMessageMenu = (props) => , -}: ThreadListItemContentProps): React.ReactElement { - const messageTypes = getUIKitMessageTypes(); +interface CustomMessageItemBodyType { + customSubcomponentsProps?: CustomSubcomponentsProps; +} + +export default function ThreadListItemContent(props: ThreadListItemContentProps): React.ReactElement { + // Internal props + const { + className, + userId, + channel, + message, + chainTop = false, + chainBottom = false, + isReactionEnabled = false, + disableQuoteMessage = false, + replyType, + nicknamesMap, + emojiContainer, + showEdit, + showRemove, + showFileViewer, + resendMessage, + toggleReaction, + onReplyInThread, + } = props; + // Public props for customization + const { + renderSenderProfile = (props: MessageProfileProps) => , + renderMessageBody = (props: MessageBodyProps & CustomMessageItemBodyType) => , + renderMessageHeader = (props: MessageHeaderProps) => , + renderMessageMenu = (props: MessageMenuProps) => , + renderEmojiMenu = () => , + renderEmojiReactions = (props: EmojiReactionsProps) => , + renderMobileMenuOnLongPress = (props: MobileBottomSheetProps) => , + } = deleteNullish(props); + const { isMobile } = useMediaQueryContext(); const { dateLocale } = useLocalization(); const { config, eventHandlers } = useSendbirdStateContext?.() || {}; @@ -106,9 +109,7 @@ export default function ThreadListItemContent({ document.getElementById(EMOJI_MENU_ROOT_ID), ], ); - const { disableUserProfile, renderUserProfile } = useUserProfileContext(); const { deleteMessage, onBeforeDownloadFileMessage } = useThreadContext(); - const avatarRef = useRef(null); const isByMe = (userId === (message as SendableMessageType)?.sender?.userId) || ((message as SendableMessageType)?.sendingStatus === 'pending') @@ -148,44 +149,12 @@ export default function ThreadListItemContent({ >
{(!isByMe && !chainBottom) && ( - ( - member?.userId === message?.sender?.userId)?.profileUrl || message?.sender?.profileUrl || ''} - ref={avatarRef} - width="28px" - height="28px" - onClick={() => { - if (!disableUserProfile) { - toggleDropdown?.(); - } - }} - /> - )} - menuItems={(closeDropdown) => ( - renderUserProfile - ? renderUserProfile({ - user: message?.sender, - close: closeDropdown, - currentUserId: userId, - avatarRef, - }) - : ( - - - - ) - )} - /> + renderSenderProfile({ + ...props, + className: 'sendbird-thread-list-item-content__left__avatar', + isByMe, + displayThreadReplies: false, + }) )} {(isByMe && !isMobile) && (
- {(!isByMe && !chainTop && !useReplying) && ( - - )} + { + (!isByMe && !chainTop && !useReplying) && renderMessageHeader(props) + }
{/* message status component */} {(isByMe && !chainBottom) && ( @@ -252,77 +211,34 @@ export default function ThreadListItemContent({
)} {/* message item body components */} - {isOgMessageEnabledInGroupChannel && isOGMessage(message as UserMessage) - ? ( - ) : isTextMessage(message as UserMessage) && ( - - )} - {isVoiceMessage(message as FileMessage) && ( - - )} - {(getUIKitMessageType((message as FileMessage)) === messageTypes.FILE) && ( - - )} - { - isMultipleFilesMessage(message) && ( - - ) - } - {(isThumbnailMessage(message as FileMessage)) && ( - - )} - {(getUIKitMessageType((message as FileMessage)) === messageTypes.UNKNOWN) && ( - - )} + {renderMessageBody({ + className: 'sendbird-thread-list-item-content__middle__message-item-body', + message, + channel, + showFileViewer, + mouseHover: false, + isMobile, + config, + isReactionEnabledInChannel, + isByMe, + onBeforeDownloadFileMessage, + /** This is for internal customization to keep the legacy */ + customSubcomponentsProps: { + ThumbnailMessageItemBody: { + style: { + width: isMobile ? '100%' : '200px', + height: '148px', + }, + }, + MultipleFilesMessageItemBody: { + threadMessageKindKey, + statefulFileInfoList, + }, + }, + // TODO: Support these props in Thread + // onMessageHeightChange, + // onTemplateMessageRenderedCallback, + })} {/* reactions */} {(isReactionEnabledInChannel && message?.reactions?.length > 0) && (
- + { + renderEmojiReactions({ + userId, + message: message as SendableMessageType, + channel, + isByMe, + emojiContainer, + memberNicknamesMap: nicknamesMap, + toggleReaction, + onPressUserProfile: onPressUserProfileHandler, + }) + }
)} {(!isByMe && !chainBottom) && ( @@ -386,24 +304,24 @@ export default function ThreadListItemContent({ )}
{showMobileMenu && ( - { + renderMobileMenuOnLongPress({ + parentRef: mobileMenuRef, + channel, + message, + userId, + replyType, + hideMenu: () => { setShowMobileMenu(false); - }} - isReactionEnabled={isReactionEnabled} - isByMe={isByMe} - emojiContainer={emojiContainer} - showEdit={showEdit} - showRemove={showRemove} - toggleReaction={toggleReaction} - isOpenedFromThread - deleteMessage={deleteMessage} - onDownloadClick={async (e) => { + }, + isReactionEnabled, + isByMe, + emojiContainer, + showEdit, + showRemove, + toggleReaction, + isOpenedFromThread: true, + deleteMessage, + onDownloadClick: async (e) => { if (!onBeforeDownloadFileMessage) return; try { @@ -415,8 +333,8 @@ export default function ThreadListItemContent({ } catch (err) { logger?.error?.('ThreadListItemContent: Error occurred while determining download continuation:', err); } - }} - /> + }, + }) )}
); diff --git a/src/ui/MessageContent/MessageBody/index.tsx b/src/ui/MessageContent/MessageBody/index.tsx index 6f436e4df..b8e3f93de 100644 --- a/src/ui/MessageContent/MessageBody/index.tsx +++ b/src/ui/MessageContent/MessageBody/index.tsx @@ -23,10 +23,16 @@ import { match } from 'ts-pattern'; import TemplateMessageItemBody from '../../TemplateMessageItemBody'; import type { OnBeforeDownloadFileMessageType } from '../../../modules/GroupChannel/context/GroupChannelProvider'; +export type CustomSubcomponentsProps = Record< + 'ThumbnailMessageItemBody' | 'MultipleFilesMessageItemBody', + Record +>; + const MESSAGE_ITEM_BODY_CLASSNAME = 'sendbird-message-content__middle__message-item-body'; export type RenderedTemplateBodyType = 'failed' | 'composite' | 'simple'; export interface MessageBodyProps { + className?: string; channel: Nullable; message: CoreMessageType; showFileViewer?: (bool: boolean) => void; @@ -43,6 +49,7 @@ export interface MessageBodyProps { export const MessageBody = (props: MessageBodyProps) => { const { + className = MESSAGE_ITEM_BODY_CLASSNAME, message, channel, showFileViewer, @@ -56,6 +63,8 @@ export const MessageBody = (props: MessageBodyProps) => { isReactionEnabledInChannel, isByMe, } = props; + // Private props for internal customization. + const customSubcomponentsProps: CustomSubcomponentsProps = props['customSubcomponentsProps'] ?? {}; const threadMessageKindKey = useThreadMessageKindKeySelector({ isMobile, @@ -68,7 +77,7 @@ export const MessageBody = (props: MessageBodyProps) => { return match(message) .when(isTemplateMessage, () => ( { .when((message) => isOgMessageEnabledInGroupChannel && isSendableMessage(message) && isOGMessage(message), () => ( - + )) .when(isTextMessage, () => ( { )) .when((message) => getUIKitMessageType(message) === messageTypes.FILE, () => ( { )) .when(isMultipleFilesMessage, () => ( { threadMessageKindKey={threadMessageKindKey} statefulFileInfoList={statefulFileInfoList} onBeforeDownloadFileMessage={onBeforeDownloadFileMessage} + {...customSubcomponentsProps['MultipleFilesMessageItemBody'] ?? {}} /> )) .when(isVoiceMessage, () => ( { )) .when(isThumbnailMessage, () => ( )) .otherwise((message) => ( { - const { - message, - channel, - userId, - chainBottom = false, - isByMe, - displayThreadReplies, - bottom, - } = props; +export function MessageProfile({ + // Internal props + className = '', + isByMe, + displayThreadReplies, + bottom, + // MessageContentProps + message, + channel, + userId, + chainBottom = false, +}: MessageProfileProps) { const avatarRef = useRef(null); const { disableUserProfile, renderUserProfile } = useUserProfileContext(); @@ -39,11 +39,9 @@ export const MessageProfile = (props: MessageProfileProps) => { void): ReactElement => ( member?.userId === message.sender.userId, - )?.profileUrl + channel?.members?.find((member) => member?.userId === message.sender.userId)?.profileUrl || message.sender.profileUrl || '' } @@ -82,6 +80,6 @@ export const MessageProfile = (props: MessageProfileProps) => { )} /> ); -}; +} export default MessageProfile; diff --git a/src/ui/MessageContent/index.tsx b/src/ui/MessageContent/index.tsx index 366f8d591..17ef5a954 100644 --- a/src/ui/MessageContent/index.tsx +++ b/src/ui/MessageContent/index.tsx @@ -53,7 +53,7 @@ export { MessageBody } from './MessageBody'; export { MessageHeader } from './MessageHeader'; export { MessageProfile } from './MessageProfile'; -export interface MessageContentProps { +export interface MessageContentProps extends MessageContentRenderSubComponentProps { className?: string | Array; userId: string; channel: Nullable; @@ -81,8 +81,9 @@ export interface MessageContentProps { onQuoteMessageClick?: (props: { message: SendableMessageType }) => void; onMessageHeightChange?: () => void; onBeforeDownloadFileMessage?: OnBeforeDownloadFileMessageType; +} - // For injecting customizable subcomponents +export interface MessageContentRenderSubComponentProps { renderSenderProfile?: (props: MessageProfileProps) => ReactNode; renderMessageBody?: (props: MessageBodyProps) => ReactNode; renderMessageHeader?: (props: MessageHeaderProps) => ReactNode; @@ -285,6 +286,7 @@ export function MessageContent(props: MessageContentProps): ReactElement { { renderSenderProfile({ ...props, + className: 'sendbird-message-content__left__avatar', isByMe, displayThreadReplies, bottom: totalBottom > 0 ? totalBottom + 'px' : '', From 94ebe7440b61acca51ad1ce933c2d794de9e5ada Mon Sep 17 00:00:00 2001 From: HoonBaek Date: Mon, 26 Aug 2024 10:55:48 +0900 Subject: [PATCH 2/2] Add render~~ props to the ThreadListItem comp --- .../components/ThreadList/ThreadListItem.tsx | 33 ++++++++----------- .../ThreadList/ThreadListItemContent.tsx | 4 +-- src/ui/MessageContent/index.tsx | 6 ++-- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/modules/Thread/components/ThreadList/ThreadListItem.tsx b/src/modules/Thread/components/ThreadList/ThreadListItem.tsx index a271eb9e4..33ef61916 100644 --- a/src/modules/Thread/components/ThreadList/ThreadListItem.tsx +++ b/src/modules/Thread/components/ThreadList/ThreadListItem.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState, useRef, useEffect, useLayoutEffect, ReactNode } from 'react'; +import React, { useMemo, useState, useRef, useEffect, useLayoutEffect } from 'react'; import format from 'date-fns/format'; import type { FileMessage, MultipleFilesMessage } from '@sendbird/chat/message'; @@ -21,10 +21,9 @@ import { SendableMessageType } from '../../../../utils'; import { User } from '@sendbird/chat'; import { getCaseResolvedReplyType } from '../../../../lib/utils/resolvedReplyType'; import { classnames } from '../../../../utils/utils'; -import type { MessageMenuProps } from '../../../../ui/MessageMenu'; -import type { MessageEmojiMenuProps } from '../../../../ui/MessageItemReactionMenu'; +import { MessageComponentRenderers } from '../../../../ui/MessageContent'; -export interface ThreadListItemProps { +export interface ThreadListItemProps extends MessageComponentRenderers { className?: string; message: SendableMessageType; chainTop?: boolean; @@ -32,21 +31,18 @@ export interface ThreadListItemProps { hasSeparator?: boolean; renderCustomSeparator?: (props: { message: SendableMessageType }) => React.ReactElement; handleScroll?: () => void; - renderEmojiMenu?: (props: MessageEmojiMenuProps) => ReactNode; - renderMessageMenu?: (props: MessageMenuProps) => ReactNode; } -export default function ThreadListItem({ - className, - message, - chainTop, - chainBottom, - hasSeparator, - renderCustomSeparator, - handleScroll, - renderEmojiMenu, - renderMessageMenu, -}: ThreadListItemProps): React.ReactElement { +export default function ThreadListItem(props: ThreadListItemProps): React.ReactElement { + const { + className, + message, + chainTop, + chainBottom, + hasSeparator, + renderCustomSeparator, + handleScroll, + } = props; const { stores, config } = useSendbirdStateContext(); const { isOnline, userMention, logger, groupChannel } = config; const userId = stores?.userStore?.user?.userId; @@ -237,6 +233,7 @@ export default function ThreadListItem({ )) } {/* modal */} {showRemove && ( diff --git a/src/modules/Thread/components/ThreadList/ThreadListItemContent.tsx b/src/modules/Thread/components/ThreadList/ThreadListItemContent.tsx index 1509686a0..28d147b15 100644 --- a/src/modules/Thread/components/ThreadList/ThreadListItemContent.tsx +++ b/src/modules/Thread/components/ThreadList/ThreadListItemContent.tsx @@ -31,13 +31,13 @@ import { useThreadContext } from '../../context/ThreadProvider'; import { classnames, deleteNullish } from '../../../../utils/utils'; import { MessageMenu, MessageMenuProps } from '../../../../ui/MessageMenu'; import useElementObserver from '../../../../hooks/useElementObserver'; -import type { MessageContentRenderSubComponentProps } from '../../../../ui/MessageContent'; +import type { MessageComponentRenderers } from '../../../../ui/MessageContent'; import MessageProfile, { MessageProfileProps } from '../../../../ui/MessageContent/MessageProfile'; import MessageBody, { CustomSubcomponentsProps, MessageBodyProps } from '../../../../ui/MessageContent/MessageBody'; import { MessageHeaderProps, MessageHeader } from '../../../../ui/MessageContent/MessageHeader'; import { MobileBottomSheetProps } from '../../../../ui/MobileMenu/types'; -export interface ThreadListItemContentProps extends MessageContentRenderSubComponentProps { +export interface ThreadListItemContentProps extends MessageComponentRenderers { className?: string; userId: string; channel: GroupChannel; diff --git a/src/ui/MessageContent/index.tsx b/src/ui/MessageContent/index.tsx index 17ef5a954..abf4af841 100644 --- a/src/ui/MessageContent/index.tsx +++ b/src/ui/MessageContent/index.tsx @@ -53,7 +53,7 @@ export { MessageBody } from './MessageBody'; export { MessageHeader } from './MessageHeader'; export { MessageProfile } from './MessageProfile'; -export interface MessageContentProps extends MessageContentRenderSubComponentProps { +export interface MessageContentProps extends MessageComponentRenderers { className?: string | Array; userId: string; channel: Nullable; @@ -83,7 +83,7 @@ export interface MessageContentProps extends MessageContentRenderSubComponentPro onBeforeDownloadFileMessage?: OnBeforeDownloadFileMessageType; } -export interface MessageContentRenderSubComponentProps { +export interface MessageComponentRenderers { renderSenderProfile?: (props: MessageProfileProps) => ReactNode; renderMessageBody?: (props: MessageBodyProps) => ReactNode; renderMessageHeader?: (props: MessageHeaderProps) => ReactNode; @@ -337,7 +337,7 @@ export function MessageContent(props: MessageContentProps): ReactElement { ref={contentRef} > { - !isByMe && !chainTop && !useReplying && renderMessageHeader(props) + (!isByMe && !chainTop && !useReplying) && renderMessageHeader(props) } {/* quote message */} {(useReplying) ? (