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) &&