Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit 5b64124

Browse files
committed
refactor: use typing event to indicate bot is responding
1 parent a1e5053 commit 5b64124

File tree

5 files changed

+46
-78
lines changed

5 files changed

+46
-78
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import BotProfileImage from './BotProfileImage';
22
import { TypingBubble } from '../foundation/components/TypingBubble';
33

4-
function CustomTypingIndicatorBubble() {
4+
function BotTypingIndicator() {
55
return (
66
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 8, marginTop: 16 }}>
77
<BotProfileImage size={28} />
@@ -10,4 +10,4 @@ function CustomTypingIndicatorBubble() {
1010
);
1111
}
1212

13-
export default CustomTypingIndicatorBubble;
13+
export default BotTypingIndicator;

src/components/CustomMessage.tsx

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import BotMessageWithBodyInput from './BotMessageWithBodyInput';
88
import { useChatContext } from './chat/context/ChatProvider';
99
import CurrentUserMessage from './CurrentUserMessage';
1010
import CustomMessageBody from './CustomMessageBody';
11-
import CustomTypingIndicatorBubble from './CustomTypingIndicatorBubble';
1211
import FileMessage from './FileMessage';
1312
import { CarouselMessage } from './messages/CarouselMessage';
1413
import FormMessage from './messages/FormMessage';
@@ -25,20 +24,19 @@ import { isSentBy } from '../utils/messages';
2524

2625
type Props = {
2726
message: BaseMessage;
28-
activeSpinnerId: number;
2927
chainTop?: boolean;
3028
chainBottom?: boolean;
3129
};
3230

3331
export default function CustomMessage(props: Props) {
32+
const { message } = props;
33+
3434
const { botUser } = useChatContext();
35-
const { message, activeSpinnerId } = props;
3635
const { replacementTextList, enableEmojiFeedback } = useConstantState();
3736
const { userId: currentUserId } = useWidgetSession();
3837
const getCarouselItems = useCarouselItems(message);
3938

4039
const botUserId = botUser?.userId;
41-
const isWaitingForBotReply = activeSpinnerId === message.messageId && !!botUser;
4240

4341
const shouldRenderFeedback = () => {
4442
return (
@@ -61,28 +59,8 @@ export default function CustomMessage(props: Props) {
6159

6260
// Sent by current user
6361
if (isSentBy(message, currentUserId)) {
64-
if (message.isUserMessage()) {
65-
/**
66-
* If a message to render is sent by me and is a last message,
67-
* typing indicator bubble is displayed below to indicate
68-
* a reply message from bot is expected to arrive.
69-
*/
70-
return (
71-
<div>
72-
<CurrentUserMessage message={message} />
73-
{isWaitingForBotReply && <CustomTypingIndicatorBubble />}
74-
</div>
75-
);
76-
}
77-
78-
if (message.isFileMessage()) {
79-
return (
80-
<div>
81-
<OutgoingFileMessage message={message} />
82-
{isWaitingForBotReply && <CustomTypingIndicatorBubble />}
83-
</div>
84-
);
85-
}
62+
if (message.isUserMessage()) return <CurrentUserMessage message={message} />;
63+
if (message.isFileMessage()) return <OutgoingFileMessage message={message} />;
8664
}
8765

8866
// Sent by bot user
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { GroupChannelHandler } from '@sendbird/chat/groupChannel';
2+
import { useEffect, useState } from 'react';
3+
4+
import { useChatContext } from '../context/ChatProvider';
5+
6+
export const useIsBotTyping = () => {
7+
const { sdk, channel, botUser, scrollSource } = useChatContext();
8+
const [isBotTyping, setIsBotTyping] = useState(false);
9+
10+
useEffect(() => {
11+
if (sdk?.groupChannel?.addGroupChannelHandler) {
12+
const handler = new GroupChannelHandler({
13+
onTypingStatusUpdated(it) {
14+
if (it.url === channel?.url) {
15+
const typing = it.getTypingUsers().some((user) => user.userId === botUser?.userId);
16+
if (typing) scrollSource.scrollPubSub.publish('scrollToBottom', { animated: true });
17+
setIsBotTyping(typing);
18+
}
19+
},
20+
});
21+
22+
const id = 'bot-typing';
23+
sdk.groupChannel.addGroupChannelHandler(id, handler);
24+
return () => sdk.groupChannel.removeGroupChannelHandler(id);
25+
}
26+
}, [sdk, botUser]);
27+
28+
return isBotTyping;
29+
};

src/components/chat/hooks/useTypingTargetMessageId.ts

Lines changed: 0 additions & 42 deletions
This file was deleted.

src/components/chat/ui/ChatMessageList.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import { Placeholder } from '../../../foundation/components/Placeholder';
1111
import { ScrollToBottomButton } from '../../../foundation/components/ScrollToBottomButton';
1212
import { isDashboardPreview } from '../../../utils';
1313
import { getMessageGrouping } from '../../../utils/messages';
14+
import BotTypingIndicator from '../../BotTypingIndicator';
1415
import CustomMessage from '../../CustomMessage';
1516
import MessageDataContent from '../../MessageDataContent';
1617
import SuggestedRepliesContainer from '../../SuggestedRepliesContainer';
1718
import { useChatContext } from '../context/ChatProvider';
1819
import { useBotStudioView } from '../hooks/useBotStudioView';
19-
import { useTypingTargetMessageId } from '../hooks/useTypingTargetMessageId';
20+
import { useIsBotTyping } from '../hooks/useIsBotTyping';
2021

2122
export const ChatMessageList = () => {
2223
const { channel, dataSource, scrollSource, handlers } = useChatContext();
@@ -29,7 +30,7 @@ export const ChatMessageList = () => {
2930
messageStackDirection = 'bottom',
3031
} = useConstantState();
3132

32-
const typingTargetMessageId = useTypingTargetMessageId();
33+
const isBotTyping = useIsBotTyping();
3334
const { filteredMessages, shouldShowOriginalDate, renderBotStudioWelcomeMessages } = useBotStudioView();
3435

3536
const render = () => {
@@ -55,6 +56,13 @@ export const ChatMessageList = () => {
5556
onLoadNext={dataSource.loadNext}
5657
depsForResetScrollPositionToBottom={[dataSource.initialized, dataSource.messages.length !== 0]}
5758
messageTopArea={renderBotStudioWelcomeMessages()}
59+
messageBottomArea={
60+
isBotTyping && (
61+
<div style={{ padding: '0 16px' }}>
62+
<BotTypingIndicator />
63+
</div>
64+
)
65+
}
5866
renderMessage={({ message, index }) => {
5967
const prevCreatedAt = filteredMessages[index - 1]?.createdAt ?? 0;
6068
const suggestedReplies = message.suggestedReplies ?? [];
@@ -79,12 +87,7 @@ export const ChatMessageList = () => {
7987
/>
8088
)}
8189
<div style={{ marginBottom: index === filteredMessages.length - 1 ? 0 : 16 }}>
82-
<CustomMessage
83-
message={message as any}
84-
activeSpinnerId={typingTargetMessageId}
85-
chainTop={top}
86-
chainBottom={bottom}
87-
/>
90+
<CustomMessage message={message as any} chainTop={top} chainBottom={bottom} />
8891

8992
{message.data &&
9093
isDashboardPreview(customUserAgentParam) &&

0 commit comments

Comments
 (0)