diff --git a/src/components/CustomTypingIndicatorBubble.tsx b/src/components/BotTypingIndicator.tsx similarity index 77% rename from src/components/CustomTypingIndicatorBubble.tsx rename to src/components/BotTypingIndicator.tsx index ca910cf58..28854cb77 100644 --- a/src/components/CustomTypingIndicatorBubble.tsx +++ b/src/components/BotTypingIndicator.tsx @@ -1,7 +1,7 @@ import BotProfileImage from './BotProfileImage'; import { TypingBubble } from '../foundation/components/TypingBubble'; -function CustomTypingIndicatorBubble() { +function BotTypingIndicator() { return (
@@ -10,4 +10,4 @@ function CustomTypingIndicatorBubble() { ); } -export default CustomTypingIndicatorBubble; +export default BotTypingIndicator; diff --git a/src/components/CustomMessage.tsx b/src/components/CustomMessage.tsx index 3094a6414..711550852 100644 --- a/src/components/CustomMessage.tsx +++ b/src/components/CustomMessage.tsx @@ -8,7 +8,6 @@ import BotMessageWithBodyInput from './BotMessageWithBodyInput'; import { useChatContext } from './chat/context/ChatProvider'; import CurrentUserMessage from './CurrentUserMessage'; import CustomMessageBody from './CustomMessageBody'; -import CustomTypingIndicatorBubble from './CustomTypingIndicatorBubble'; import FileMessage from './FileMessage'; import { CarouselMessage } from './messages/CarouselMessage'; import FormMessage from './messages/FormMessage'; @@ -25,20 +24,19 @@ import { isSentBy } from '../utils/messages'; type Props = { message: BaseMessage; - activeSpinnerId: number; chainTop?: boolean; chainBottom?: boolean; }; export default function CustomMessage(props: Props) { + const message = props.message; + const { botUser } = useChatContext(); - const { message, activeSpinnerId } = props; const { replacementTextList, enableEmojiFeedback } = useConstantState(); const { userId: currentUserId } = useWidgetSession(); const getCarouselItems = useCarouselItems(message); const botUserId = botUser?.userId; - const isWaitingForBotReply = activeSpinnerId === message.messageId && !!botUser; const shouldRenderFeedback = () => { return ( @@ -61,28 +59,8 @@ export default function CustomMessage(props: Props) { // Sent by current user if (isSentBy(message, currentUserId)) { - if (message.isUserMessage()) { - /** - * If a message to render is sent by me and is a last message, - * typing indicator bubble is displayed below to indicate - * a reply message from bot is expected to arrive. - */ - return ( -
- - {isWaitingForBotReply && } -
- ); - } - - if (message.isFileMessage()) { - return ( -
- - {isWaitingForBotReply && } -
- ); - } + if (message.isUserMessage()) return ; + if (message.isFileMessage()) return ; } // Sent by bot user diff --git a/src/components/chat/hooks/useIsBotTyping.ts b/src/components/chat/hooks/useIsBotTyping.ts new file mode 100644 index 000000000..cb9e0a5a9 --- /dev/null +++ b/src/components/chat/hooks/useIsBotTyping.ts @@ -0,0 +1,29 @@ +import { GroupChannelHandler } from '@sendbird/chat/groupChannel'; +import { useEffect, useState } from 'react'; + +import { useChatContext } from '../context/ChatProvider'; + +export const useIsBotTyping = () => { + const { sdk, channel, botUser, scrollSource } = useChatContext(); + const [isBotTyping, setIsBotTyping] = useState(false); + + useEffect(() => { + if (sdk?.groupChannel?.addGroupChannelHandler) { + const handler = new GroupChannelHandler({ + onTypingStatusUpdated(it) { + if (it.url === channel?.url) { + const typing = it.getTypingUsers().some((user) => user.userId === botUser?.userId); + if (typing) scrollSource.scrollPubSub.publish('scrollToBottom', { animated: true }); + setIsBotTyping(typing); + } + }, + }); + + const id = 'bot-typing'; + sdk.groupChannel.addGroupChannelHandler(id, handler); + return () => sdk.groupChannel.removeGroupChannelHandler(id); + } + }, [sdk, botUser]); + + return isBotTyping; +}; diff --git a/src/components/chat/hooks/useTypingTargetMessageId.ts b/src/components/chat/hooks/useTypingTargetMessageId.ts deleted file mode 100644 index 629543656..000000000 --- a/src/components/chat/hooks/useTypingTargetMessageId.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { SendingStatus } from '@sendbird/chat/message'; -import { useEffect, useState } from 'react'; - -import { useWidgetSession } from '../../../context/WidgetSettingContext'; -import { isSentBy } from '../../../utils/messages'; -import { useChatContext } from '../context/ChatProvider'; - -/** - * If the updated last message was sent by the current user, indicate a typing bubble for the sent message. - * If the updated last message is pending or failed and was sent by the current user, or if it was sent by the bot, deactivate the typing bubble. - */ -export const useTypingTargetMessageId = () => { - const { userId } = useWidgetSession(); - const { channel, dataSource, scrollSource } = useChatContext(); - const [messageId, setMessageId] = useState(-1); - const lastMessage = dataSource.messages[dataSource.messages.length - 1]; - - useEffect(() => { - if (lastMessage) { - const shouldActivateSpinner = - isSentBy(lastMessage, userId) && - (lastMessage.isUserMessage() || lastMessage.isFileMessage()) && - lastMessage.sendingStatus === SendingStatus.SUCCEEDED && - channel?.memberCount === 2; - - setMessageId(shouldActivateSpinner ? lastMessage.messageId : -1); - setTimeout(() => scrollSource.scrollPubSub.publish('scrollToBottom', {}), 150); - } - }, [lastMessage?.messageId]); - - // useGroupChannelHandler(sdk, { - // onTypingStatusUpdated: (it) => { - // if (it.url === channel?.url) { - // const shouldActivateSpinner = it.getTypingUsers().find((it) => it.userId === botId); - // setMessageId(shouldActivateSpinner ? lastMessage.messageId : -1); - // setTimeout(() => scrollSource.scrollPubSub.publish('scrollToBottom', {}), 150); - // } - // }, - // }); - - return messageId; -}; diff --git a/src/components/chat/ui/ChatMessageList.tsx b/src/components/chat/ui/ChatMessageList.tsx index fd05608e3..84e1a49db 100644 --- a/src/components/chat/ui/ChatMessageList.tsx +++ b/src/components/chat/ui/ChatMessageList.tsx @@ -11,18 +11,19 @@ import { Placeholder } from '../../../foundation/components/Placeholder'; import { ScrollToBottomButton } from '../../../foundation/components/ScrollToBottomButton'; import { isDashboardPreview } from '../../../utils'; import { getMessageGrouping } from '../../../utils/messages'; +import BotTypingIndicator from '../../BotTypingIndicator'; import CustomMessage from '../../CustomMessage'; import MessageDataContent from '../../MessageDataContent'; import SuggestedRepliesContainer from '../../SuggestedRepliesContainer'; import { useChatContext } from '../context/ChatProvider'; import { useBotStudioView } from '../hooks/useBotStudioView'; -import { useTypingTargetMessageId } from '../hooks/useTypingTargetMessageId'; +import { useIsBotTyping } from '../hooks/useIsBotTyping'; export const ChatMessageList = () => { const { channel, dataSource, scrollSource, handlers } = useChatContext(); const { botStudioEditProps, customUserAgentParam, stringSet, dateLocale, enableMessageGrouping } = useConstantState(); - const typingTargetMessageId = useTypingTargetMessageId(); + const isBotTyping = useIsBotTyping(); const { filteredMessages, shouldShowOriginalDate, renderBotStudioWelcomeMessages } = useBotStudioView(); const render = () => { @@ -48,6 +49,13 @@ export const ChatMessageList = () => { onLoadNext={dataSource.loadNext} depsForResetScrollPositionToBottom={[dataSource.initialized, dataSource.messages.length !== 0]} messageTopArea={renderBotStudioWelcomeMessages()} + messageBottomArea={ + isBotTyping && ( +
+ +
+ ) + } renderMessage={({ message, index }) => { const prevCreatedAt = filteredMessages[index - 1]?.createdAt ?? 0; const suggestedReplies = message.suggestedReplies ?? []; @@ -72,12 +80,7 @@ export const ChatMessageList = () => { /> )}
- + {message.data && isDashboardPreview(customUserAgentParam) &&