From 400afb068fc295da98f5551506410c5955851c4f Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 20 Oct 2024 19:17:35 +0800 Subject: [PATCH 01/23] refactor: Minor code improvements --- src/components/ChatBotContainer.tsx | 4 ++-- src/hooks/internal/useBotEffectsInternal.tsx | 20 +++----------------- src/hooks/internal/useChatWindowInternal.ts | 11 +++++++++-- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/components/ChatBotContainer.tsx b/src/components/ChatBotContainer.tsx index b193172b..68f24a63 100644 --- a/src/components/ChatBotContainer.tsx +++ b/src/components/ChatBotContainer.tsx @@ -8,7 +8,7 @@ import ChatBotButton from "./ChatBotButton/ChatBotButton"; import ChatBotTooltip from "./ChatBotTooltip/ChatBotTooltip"; import { useButtonInternal } from "../hooks/internal/useButtonsInternal"; import { useChatWindowInternal } from "../hooks/internal/useChatWindowInternal"; -import { useBotEffectInternal } from "../hooks/internal/useBotEffectsInternal"; +import { useBotEffectsInternal } from "../hooks/internal/useBotEffectsInternal"; import { useIsDesktopInternal } from "../hooks/internal/useIsDesktopInternal"; import { useBotRefsContext } from "../context/BotRefsContext"; import { useBotStatesContext } from "../context/BotStatesContext"; @@ -59,7 +59,7 @@ const ChatBotContainer = ({ const { headerButtons, chatInputButtons, footerButtons } = useButtonInternal(); // loads all use effects - useBotEffectInternal(); + useBotEffectsInternal(); /** * Retrieves class name for window state. diff --git a/src/hooks/internal/useBotEffectsInternal.tsx b/src/hooks/internal/useBotEffectsInternal.tsx index 7e440e83..a6dff32a 100644 --- a/src/hooks/internal/useBotEffectsInternal.tsx +++ b/src/hooks/internal/useBotEffectsInternal.tsx @@ -25,7 +25,7 @@ import { Params } from "../../types/Params"; /** * Internal custom hook for common use effects. */ -export const useBotEffectInternal = () => { +export const useBotEffectsInternal = () => { // handles platform const isDesktop = useIsDesktopInternal(); @@ -53,7 +53,6 @@ export const useBotEffectInternal = () => { isChatWindowOpen, isBotTyping, isScrolling, - timeoutId, hasFlowStarted, setIsChatWindowOpen, setTextAreaDisabled, @@ -73,7 +72,7 @@ export const useBotEffectInternal = () => { const { viewportHeight, setViewportHeight, setViewportWidth, openChat } = useChatWindowInternal(); // handles notifications - const { playNotificationSound, setUnreadCount, setUpNotifications } = useNotificationInternal(); + const { playNotificationSound, setUpNotifications } = useNotificationInternal(); // handles user first interaction const { handleFirstInteraction } = useFirstInteractionInternal(); @@ -103,13 +102,9 @@ export const useBotEffectInternal = () => { }; }, []); - // default setup for notifications + // default setup for notifications, text area, chat window, audio and voice useEffect(() => { setUpNotifications(); - }, []) - - // default setup for text area, chat window, audio and voice - useEffect(() => { setTextAreaDisabled(settings.chatInput?.disabled as boolean); setIsChatWindowOpen(settings.chatWindow?.defaultOpen as boolean); setAudioToggledOn(settings.audio?.defaultToggledOn as boolean); @@ -196,13 +191,6 @@ export const useBotEffectInternal = () => { playNotificationSound(); }, [messages.length]); - // resets unread count on opening chat - useEffect(() => { - if (isChatWindowOpen) { - setUnreadCount(0); - } - }, [isChatWindowOpen]); - // handles scrolling/resizing window on mobile devices useEffect(() => { if (isDesktop) { @@ -293,6 +281,4 @@ export const useBotEffectInternal = () => { goToPath("start"); } }, [hasFlowStarted, settings.general?.flowStartTrigger]); - - return { timeoutId } }; diff --git a/src/hooks/internal/useChatWindowInternal.ts b/src/hooks/internal/useChatWindowInternal.ts index 51d6be0f..c88f1f8a 100644 --- a/src/hooks/internal/useChatWindowInternal.ts +++ b/src/hooks/internal/useChatWindowInternal.ts @@ -19,7 +19,8 @@ export const useChatWindowInternal = () => { viewportHeight, setViewportHeight, viewportWidth, - setViewportWidth + setViewportWidth, + setUnreadCount } = useBotStatesContext(); // handles rcb events @@ -42,7 +43,13 @@ export const useChatWindowInternal = () => { return; } } - setIsChatWindowOpen(prev => !prev); + setIsChatWindowOpen(prev => { + // if currently false means opening so set unread count to 0 + if (!prev) { + setUnreadCount(0); + } + return !prev; + }); }, []); /** From faad787258a5256fec0ab68bc14d82c63c9ea05e Mon Sep 17 00:00:00 2001 From: Rahul RK <47377566+rahulrk-dev@users.noreply.github.com> Date: Sun, 20 Oct 2024 21:09:06 +0530 Subject: [PATCH 02/23] Add test cases for message context (#237) --- __tests__/context/MessagesContext.test.tsx | 114 +++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 __tests__/context/MessagesContext.test.tsx diff --git a/__tests__/context/MessagesContext.test.tsx b/__tests__/context/MessagesContext.test.tsx new file mode 100644 index 00000000..37b6ad7e --- /dev/null +++ b/__tests__/context/MessagesContext.test.tsx @@ -0,0 +1,114 @@ +import React from 'react' +import { expect } from '@jest/globals' +import { act, render, screen } from '@testing-library/react' +import '@testing-library/jest-dom/jest-globals' + +import { + useMessagesContext, + MessagesProvider, +} from '../../src/context/MessagesContext' + +const TestComponent = () => { + const { messages, setMessages } = useMessagesContext() + + const handleAddMessage = () => { + setMessages([ + ...messages, + { + id: '1', + content: 'Hello World!', + sender: 'user1', + type: 'message', + timestamp: new Date().toUTCString(), + }, + ]) + } + + const handleClearMessage = () => { + setMessages([]) + } + + return ( +
+

+ Messages: {messages.map((message) => message.content).join(', ')} +

+

Messages Count: {messages.length}

+ + + +
+ ) +} + +describe('MessagesContext', () => { + it('provides the correct default values', () => { + render( + + + + ) + + expect(screen.getByTestId('messages')).toHaveTextContent(`Messages:`) + expect(screen.getByTestId('messagesCount')).toHaveTextContent( + `Messages Count: 0` + ) + }) + + it('allows adding messages in the context', () => { + render( + + + + ) + + const addMessageBtn = screen.getByText('Add Message') + + act(() => { + addMessageBtn.click() + }) + + expect(screen.getByTestId('messages')).toHaveTextContent( + `Messages: Hello World!` + ) + expect(screen.getByTestId('messagesCount')).toHaveTextContent( + `Messages Count: 1` + ) + + act(() => { + addMessageBtn.click() + }) + + expect(screen.getByTestId('messagesCount')).toHaveTextContent( + `Messages Count: 2` + ) + }) + + it('allows updating messages in the context', () => { + render( + + + + ) + + const clearMessageBtn = screen.getByText('Clear Message') + const addMessageBtn = screen.getByText('Add Message') + + act(() => { + clearMessageBtn.click() + }) + + expect(screen.getByTestId('messages')).toHaveTextContent(`Messages:`) + expect(screen.getByTestId('messagesCount')).toHaveTextContent( + `Messages Count: 0` + ) + + act(() => { + addMessageBtn.click() + }) + + expect(screen.getByTestId('messagesCount')).toHaveTextContent( + `Messages Count: 1` + ) + }) +}) From 56fcc12a50d7139363931974618663f7e869449e Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 20 Oct 2024 23:17:53 +0800 Subject: [PATCH 03/23] refactor: Improve desktop and mobile devices check --- src/hooks/internal/useIsDesktopInternal.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/hooks/internal/useIsDesktopInternal.ts b/src/hooks/internal/useIsDesktopInternal.ts index 9d1279df..d0ef4603 100644 --- a/src/hooks/internal/useIsDesktopInternal.ts +++ b/src/hooks/internal/useIsDesktopInternal.ts @@ -5,7 +5,12 @@ export const useIsDesktopInternal = () => { if (typeof window === 'undefined' || !window.navigator) { return false; // Default to false if running on server-side } - return !(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)); + const userAgent = navigator.userAgent; + const isNotMobileUA = !(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)); + const isWideEnough = window.innerWidth >= 768; + + // device is desktop if it is not a mobile agent and if the width is wide enough + return isNotMobileUA && isWideEnough; }, []); // boolean indicating if user is on desktop (otherwise treated as on mobile) From 79380478cd1ae0bb8c09829d5d2fc30c069063a3 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 20 Oct 2024 23:19:05 +0800 Subject: [PATCH 04/23] lint: Fix lint issues --- src/hooks/internal/useIsDesktopInternal.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks/internal/useIsDesktopInternal.ts b/src/hooks/internal/useIsDesktopInternal.ts index d0ef4603..cd1fe2ac 100644 --- a/src/hooks/internal/useIsDesktopInternal.ts +++ b/src/hooks/internal/useIsDesktopInternal.ts @@ -6,11 +6,11 @@ export const useIsDesktopInternal = () => { return false; // Default to false if running on server-side } const userAgent = navigator.userAgent; - const isNotMobileUA = !(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)); - const isWideEnough = window.innerWidth >= 768; + const isNotMobileUA = !(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)); + const isWideEnough = window.innerWidth >= 768; - // device is desktop if it is not a mobile agent and if the width is wide enough - return isNotMobileUA && isWideEnough; + // device is desktop if it is not a mobile agent and if the width is wide enough + return isNotMobileUA && isWideEnough; }, []); // boolean indicating if user is on desktop (otherwise treated as on mobile) From 37a433e15e1ed28d1b2478f621d086d47ec50656 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Sun, 20 Oct 2024 23:19:27 +0800 Subject: [PATCH 05/23] fix: Fix icon color in chatbot footer --- src/constants/internal/DefaultSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/internal/DefaultSettings.tsx b/src/constants/internal/DefaultSettings.tsx index 22f963e1..fd790597 100644 --- a/src/constants/internal/DefaultSettings.tsx +++ b/src/constants/internal/DefaultSettings.tsx @@ -154,7 +154,7 @@ export const DefaultSettings: Settings = { background: "linear-gradient(to right, #42b0c5, #491d8d)", }} > - + React ChatBotify From 9fb2d317de7c4a03e8e2aac605097a3552383a8e Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 21 Oct 2024 01:13:05 +0800 Subject: [PATCH 06/23] refactor: Remove exposing setters directly in hooks --- __tests__/hooks/internal/useSettingsInternal.test.ts | 7 +++++-- src/hooks/internal/useBotEffectsInternal.tsx | 4 ++-- src/hooks/internal/useMessagesInternal.ts | 9 ++++++++- src/hooks/internal/usePathsInternal.ts | 9 ++++++++- src/hooks/internal/useSettingsInternal.ts | 9 ++++++++- src/hooks/internal/useStylesInternal.ts | 9 ++++++++- src/hooks/internal/useToastsInternal.ts | 9 ++++++++- src/hooks/useMessages.ts | 4 ++-- src/hooks/usePaths.ts | 4 ++-- src/hooks/useSettings.ts | 4 ++-- src/hooks/useStyles.ts | 4 ++-- src/hooks/useToasts.ts | 4 ++-- 12 files changed, 57 insertions(+), 19 deletions(-) diff --git a/__tests__/hooks/internal/useSettingsInternal.test.ts b/__tests__/hooks/internal/useSettingsInternal.test.ts index 79860b5c..84c247e5 100644 --- a/__tests__/hooks/internal/useSettingsInternal.test.ts +++ b/__tests__/hooks/internal/useSettingsInternal.test.ts @@ -17,6 +17,7 @@ jest.mock("../../../src/utils/configParser", () => ({ describe("useSettingsInternal", () => { const mockSetSettings = jest.fn(); + const mockReplaceSettings = jest.fn(); const mockSettings: Settings = { general: { primaryColor: "red" } }; beforeEach(() => { @@ -24,15 +25,17 @@ describe("useSettingsInternal", () => { (useSettingsContext as jest.Mock).mockReturnValue({ settings: mockSettings, setSettings: mockSetSettings, + replaceSettings: mockReplaceSettings, }); }); - it("should return settings, setSettings and updateSettings method", () => { + it("should return settings, replaceSettings and updateSettings method", () => { const { result } = renderHook(() => useSettingsInternal()); expect(result.current.settings).toEqual(mockSettings); - expect(result.current.setSettings).toBe(mockSetSettings); + expect(result.current.replaceSettings).toBeDefined(); expect(result.current.updateSettings).toBeDefined(); + expect(typeof result.current.replaceSettings).toBe("function"); expect(typeof result.current.updateSettings).toBe("function"); }); diff --git a/src/hooks/internal/useBotEffectsInternal.tsx b/src/hooks/internal/useBotEffectsInternal.tsx index a6dff32a..acd17bb1 100644 --- a/src/hooks/internal/useBotEffectsInternal.tsx +++ b/src/hooks/internal/useBotEffectsInternal.tsx @@ -39,7 +39,7 @@ export const useBotEffectsInternal = () => { removeMessage, streamMessage, messages, - setMessages + replaceMessages, } = useMessagesInternal(); // handles paths @@ -125,7 +125,7 @@ export const useBotEffectsInternal = () => { if (chatHistory != null) { // note: must always render this button even if autoload (chat history logic relies on system message) const messageContent = createMessage(, "system"); - setMessages([messageContent]); + replaceMessages([messageContent]); if (settings.chatHistory?.autoLoad) { showChatHistory(); } diff --git a/src/hooks/internal/useMessagesInternal.ts b/src/hooks/internal/useMessagesInternal.ts index e9bd5357..9d340515 100644 --- a/src/hooks/internal/useMessagesInternal.ts +++ b/src/hooks/internal/useMessagesInternal.ts @@ -255,12 +255,19 @@ export const useMessagesInternal = () => { return true; }, [callRcbEvent, messages, settings.event?.rcbStopStreamMessage, streamMessageMap]) + /** + * Replaces (overwrites entirely) the current messages with the new messages. + */ + const replaceMessages = (newMessages: Array) => { + setMessages(newMessages); + } + return { endStreamMessage, injectMessage, removeMessage, streamMessage, messages, - setMessages + replaceMessages }; }; diff --git a/src/hooks/internal/usePathsInternal.ts b/src/hooks/internal/usePathsInternal.ts index c2291a0e..4e948fe3 100644 --- a/src/hooks/internal/usePathsInternal.ts +++ b/src/hooks/internal/usePathsInternal.ts @@ -74,6 +74,13 @@ export const usePathsInternal = () => { return true; }, [paths, setPaths, settings]); + /** + * Replaces (overwrites entirely) the current paths with the new paths. + */ + const replacePaths = (newPaths: Array) => { + setPaths(newPaths); + } + return { getCurrPath, getPrevPath, @@ -81,6 +88,6 @@ export const usePathsInternal = () => { blockAllowsAttachment, setBlockAllowsAttachment, paths, - setPaths + replacePaths }; }; diff --git a/src/hooks/internal/useSettingsInternal.ts b/src/hooks/internal/useSettingsInternal.ts index c1231f80..ddeef4ef 100644 --- a/src/hooks/internal/useSettingsInternal.ts +++ b/src/hooks/internal/useSettingsInternal.ts @@ -18,9 +18,16 @@ export const useSettingsInternal = () => { setSettings(deepClone(getCombinedConfig(fields, settings) as Settings)); } + /** + * Replaces (overwrites entirely) the current settings with the new settings. + */ + const replaceSettings = (newSettings: Settings) => { + setSettings(newSettings); + } + return { settings, - setSettings, + replaceSettings, updateSettings }; }; diff --git a/src/hooks/internal/useStylesInternal.ts b/src/hooks/internal/useStylesInternal.ts index 88a0d45f..1293f74e 100644 --- a/src/hooks/internal/useStylesInternal.ts +++ b/src/hooks/internal/useStylesInternal.ts @@ -18,9 +18,16 @@ export const useStylesInternal = () => { setStyles(deepClone(getCombinedConfig(fields, styles) as Styles)); } + /** + * Replaces (overwrites entirely) the current styles with the new styles. + */ + const replaceStyles = (newStyles: Styles) => { + setStyles(newStyles); + } + return { styles, - setStyles, + replaceStyles, updateStyles }; }; diff --git a/src/hooks/internal/useToastsInternal.ts b/src/hooks/internal/useToastsInternal.ts index cb6aa466..981ce6d9 100644 --- a/src/hooks/internal/useToastsInternal.ts +++ b/src/hooks/internal/useToastsInternal.ts @@ -98,10 +98,17 @@ export const useToastsInternal = () => { return id; }, [callRcbEvent, setToasts]); + /** + * Replaces (overwrites entirely) the current toasts with the new toasts. + */ + const replaceToasts = (newToasts: Array) => { + setToasts(newToasts); + } + return { showToast, dismissToast, toasts, - setToasts + replaceToasts }; }; diff --git a/src/hooks/useMessages.ts b/src/hooks/useMessages.ts index 02f453bd..733cb6bc 100644 --- a/src/hooks/useMessages.ts +++ b/src/hooks/useMessages.ts @@ -11,7 +11,7 @@ export const useMessages = () => { removeMessage, streamMessage, messages, - setMessages + replaceMessages, } = useMessagesInternal(); return { @@ -20,6 +20,6 @@ export const useMessages = () => { removeMessage, streamMessage, messages, - setMessages, + replaceMessages, }; }; diff --git a/src/hooks/usePaths.ts b/src/hooks/usePaths.ts index 3867e448..ac49bc7a 100644 --- a/src/hooks/usePaths.ts +++ b/src/hooks/usePaths.ts @@ -10,7 +10,7 @@ export const usePaths = () => { getPrevPath, goToPath, paths, - setPaths + replacePaths, } = usePathsInternal(); return { @@ -18,6 +18,6 @@ export const usePaths = () => { getPrevPath, goToPath, paths, - setPaths + replacePaths, }; }; diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 317f4b96..e3a86263 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -5,11 +5,11 @@ import { useSettingsInternal } from "./internal/useSettingsInternal"; */ export const useSettings = () => { // handles settings - const { settings, setSettings, updateSettings } = useSettingsInternal(); + const { settings, replaceSettings, updateSettings } = useSettingsInternal(); return { settings, - setSettings, + replaceSettings, updateSettings }; }; diff --git a/src/hooks/useStyles.ts b/src/hooks/useStyles.ts index 5b5943f1..d61dd96e 100644 --- a/src/hooks/useStyles.ts +++ b/src/hooks/useStyles.ts @@ -5,11 +5,11 @@ import { useStylesInternal } from "./internal/useStylesInternal"; */ export const useStyles = () => { // handles styles - const { styles, setStyles, updateStyles } = useStylesInternal(); + const { styles, replaceStyles, updateStyles } = useStylesInternal(); return { styles, - setStyles, + replaceStyles, updateStyles }; }; diff --git a/src/hooks/useToasts.ts b/src/hooks/useToasts.ts index 08205086..f54502ec 100644 --- a/src/hooks/useToasts.ts +++ b/src/hooks/useToasts.ts @@ -5,12 +5,12 @@ import { useToastsInternal } from "./internal/useToastsInternal"; */ export const useToasts = () => { // handles toasts - const { showToast, dismissToast, toasts, setToasts } = useToastsInternal(); + const { showToast, dismissToast, toasts, replaceToasts } = useToastsInternal(); return { showToast, dismissToast, toasts, - setToasts + replaceToasts }; }; From b029bdb7bbb08df81690ae89bfb16bd7207f8f3b Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 21 Oct 2024 01:44:21 +0800 Subject: [PATCH 07/23] lint: Fix lint issues --- src/hooks/internal/useToastsInternal.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks/internal/useToastsInternal.ts b/src/hooks/internal/useToastsInternal.ts index 981ce6d9..46b5d142 100644 --- a/src/hooks/internal/useToastsInternal.ts +++ b/src/hooks/internal/useToastsInternal.ts @@ -99,11 +99,11 @@ export const useToastsInternal = () => { }, [callRcbEvent, setToasts]); /** - * Replaces (overwrites entirely) the current toasts with the new toasts. - */ + * Replaces (overwrites entirely) the current toasts with the new toasts. + */ const replaceToasts = (newToasts: Array) => { - setToasts(newToasts); - } + setToasts(newToasts); + } return { showToast, From 686a3cee87ae4a641052f271bbc25bf590c729a3 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 21 Oct 2024 02:03:19 +0800 Subject: [PATCH 08/23] refactor: Minor code improvements --- src/hooks/internal/useBotEffectsInternal.tsx | 30 +------- src/hooks/internal/useFlowInternal.ts | 18 ++--- src/hooks/internal/useMessagesInternal.ts | 77 +++++++++++++++++--- src/hooks/internal/usePathsInternal.ts | 8 +- src/hooks/internal/useSettingsInternal.ts | 30 ++++---- src/hooks/internal/useStylesInternal.ts | 30 ++++---- src/hooks/internal/useToastsInternal.ts | 2 +- 7 files changed, 112 insertions(+), 83 deletions(-) diff --git a/src/hooks/internal/useBotEffectsInternal.tsx b/src/hooks/internal/useBotEffectsInternal.tsx index acd17bb1..28349799 100644 --- a/src/hooks/internal/useBotEffectsInternal.tsx +++ b/src/hooks/internal/useBotEffectsInternal.tsx @@ -4,7 +4,6 @@ import ChatHistoryButton from "../../components/ChatHistoryButton/ChatHistoryBut import { preProcessBlock } from "../../services/BlockService/BlockService"; import { saveChatHistory, setHistoryStorageValues } from "../../services/ChatHistoryService"; import { createMessage } from "../../utils/messageBuilder"; -import { isChatBotVisible } from "../../utils/displayChecker"; import { useIsDesktopInternal } from "./useIsDesktopInternal"; import { useChatWindowInternal } from "./useChatWindowInternal"; import { useNotificationInternal } from "./useNotificationsInternal"; @@ -51,8 +50,6 @@ export const useBotEffectsInternal = () => { // handles bot states const { isChatWindowOpen, - isBotTyping, - isScrolling, hasFlowStarted, setIsChatWindowOpen, setTextAreaDisabled, @@ -65,14 +62,14 @@ export const useBotEffectsInternal = () => { } = useBotStatesContext(); // handles bot refs - const { flowRef, chatBodyRef, streamMessageMap, paramsInputRef, keepVoiceOnRef } = useBotRefsContext(); + const { flowRef, streamMessageMap, paramsInputRef, keepVoiceOnRef } = useBotRefsContext(); const flow = flowRef.current as Flow; // handles chat window const { viewportHeight, setViewportHeight, setViewportWidth, openChat } = useChatWindowInternal(); // handles notifications - const { playNotificationSound, setUpNotifications } = useNotificationInternal(); + const { setUpNotifications } = useNotificationInternal(); // handles user first interaction const { handleFirstInteraction } = useFirstInteractionInternal(); @@ -168,29 +165,6 @@ export const useBotEffectsInternal = () => { }); }, [isDesktop]); - // triggers saving of chat history and checks for notifications - useEffect(() => { - saveChatHistory(messages); - - // if messages are empty or chatbot is open and user is not scrolling, no need to notify - if (messages.length === 0 || isChatWindowOpen && !isScrolling) { - return; - } - - // if chatbot is embedded and visible, no need to notify - if (settings.general?.embedded && isChatBotVisible(chatBodyRef.current as HTMLDivElement) || isBotTyping) { - return; - } - - const lastMessage = messages[messages.length - 1]; - // if message is sent by user or is bot typing or bot is embedded, return - if (!lastMessage || lastMessage.sender === "user") { - return; - } - - playNotificationSound(); - }, [messages.length]); - // handles scrolling/resizing window on mobile devices useEffect(() => { if (isDesktop) { diff --git a/src/hooks/internal/useFlowInternal.ts b/src/hooks/internal/useFlowInternal.ts index ae7f6876..645df543 100644 --- a/src/hooks/internal/useFlowInternal.ts +++ b/src/hooks/internal/useFlowInternal.ts @@ -1,21 +1,21 @@ +import { useMessagesInternal } from "./useMessagesInternal"; +import { usePathsInternal } from "./usePathsInternal"; +import { useToastsInternal } from "./useToastsInternal"; import { useBotRefsContext } from "../../context/BotRefsContext"; import { useBotStatesContext } from "../../context/BotStatesContext"; -import { useMessagesContext } from "../../context/MessagesContext"; -import { usePathsContext } from "../../context/PathsContext"; -import { useToastsContext } from "../../context/ToastsContext"; /** * Internal custom hook for managing flow. */ export const useFlowInternal = () => { // handles messages - const { setMessages } = useMessagesContext(); + const { replaceMessages } = useMessagesInternal(); // handles paths - const { setPaths } = usePathsContext(); + const { replacePaths } = usePathsInternal(); // handles toasts - const { setToasts } = useToastsContext(); + const { replaceToasts } = useToastsInternal(); // handles bot states const { hasFlowStarted } = useBotStatesContext(); @@ -27,9 +27,9 @@ export const useFlowInternal = () => { * Restarts the conversation flow for the chatbot. */ const restartFlow = () => { - setMessages([]); - setToasts([]); - setPaths(["start"]); + replaceMessages([]); + replaceToasts([]); + replacePaths(["start"]); } /** diff --git a/src/hooks/internal/useMessagesInternal.ts b/src/hooks/internal/useMessagesInternal.ts index 9d340515..966ac593 100644 --- a/src/hooks/internal/useMessagesInternal.ts +++ b/src/hooks/internal/useMessagesInternal.ts @@ -4,6 +4,8 @@ import { processAudio } from "../../services/AudioService"; import { saveChatHistory } from "../../services/ChatHistoryService"; import { createMessage } from "../../utils/messageBuilder"; import { parseMarkupMessage } from "../../utils/markupParser"; +import { isChatBotVisible } from "../../utils/displayChecker"; +import { useNotificationInternal } from "./useNotificationsInternal"; import { useRcbEventInternal } from "./useRcbEventInternal"; import { useSettingsContext } from "../../context/SettingsContext"; import { useMessagesContext } from "../../context/MessagesContext"; @@ -23,13 +25,23 @@ export const useMessagesInternal = () => { const { messages, setMessages } = useMessagesContext(); // handles bot states - const { audioToggledOn, isChatWindowOpen, setIsBotTyping, setUnreadCount } = useBotStatesContext(); + const { + audioToggledOn, + isChatWindowOpen, + isScrolling, + isBotTyping, + setIsBotTyping, + setUnreadCount + } = useBotStatesContext(); // handles bot refs - const { streamMessageMap } = useBotRefsContext(); + const { streamMessageMap, chatBodyRef } = useBotRefsContext(); // handles rcb events const { callRcbEvent } = useRcbEventInternal(); + + // handles notification + const { playNotificationSound } = useNotificationInternal(); /** * Simulates the streaming of a message from the bot. @@ -43,7 +55,11 @@ export const useMessagesInternal = () => { setIsBotTyping(false); // set an initial empty message to be used for streaming - setMessages(prevMessages => [...prevMessages, message]); + setMessages(prevMessages => { + const updatedMessages = [...prevMessages, message]; + handlePostMessagesUpdate(updatedMessages); + return updatedMessages; + }); streamMessageMap.current.set("bot", message.id); // initialize default message to empty with stream index position 0 @@ -79,6 +95,7 @@ export const useMessagesInternal = () => { break; } } + handlePostMessagesUpdate(updatedMessages); return updatedMessages; }); }, streamSpeed); @@ -129,7 +146,11 @@ export const useMessagesInternal = () => { const streamSpeed = settings.userBubble?.streamSpeed as number; await simulateStream(message, streamSpeed, useMarkup); } else { - setMessages((prevMessages) => [...prevMessages, message]); + setMessages((prevMessages) => { + const updatedMessages = [...prevMessages, message]; + handlePostMessagesUpdate(updatedMessages); + return updatedMessages; + }); } // handles post-message inject event @@ -160,7 +181,11 @@ export const useMessagesInternal = () => { } } - setMessages((prevMessages) => prevMessages.filter(message => message.id !== messageId)); + setMessages((prevMessages) => { + const updatedMessages = prevMessages.filter(message => message.id !== messageId); + handlePostMessagesUpdate(updatedMessages); + return updatedMessages; + }); setUnreadCount((prevCount) => Math.max(prevCount - 1, 0)); return messageId; }, [callRcbEvent, messages, settings.event?.rcbRemoveMessage]); @@ -185,7 +210,11 @@ export const useMessagesInternal = () => { } setIsBotTyping(false); - setMessages((prevMessages) => [...prevMessages, message]); + setMessages((prevMessages) => { + const updatedMessages = [...prevMessages, message]; + handlePostMessagesUpdate(updatedMessages); + return [...prevMessages, message]; + }); setUnreadCount(prev => prev + 1); streamMessageMap.current.set(sender, message.id); return message.id; @@ -212,7 +241,7 @@ export const useMessagesInternal = () => { break; } } - + handlePostMessagesUpdate(updatedMessages) return updatedMessages; }); return streamMessageMap.current.get(sender) ?? null; @@ -256,11 +285,37 @@ export const useMessagesInternal = () => { }, [callRcbEvent, messages, settings.event?.rcbStopStreamMessage, streamMessageMap]) /** - * Replaces (overwrites entirely) the current messages with the new messages. - */ + * Replaces (overwrites entirely) the current messages with the new messages. + */ const replaceMessages = (newMessages: Array) => { - setMessages(newMessages); - } + handlePostMessagesUpdate(newMessages); + setMessages(newMessages); + } + + /** + * Handles post messages updates such as saving chat history and playing notification sound. + */ + const handlePostMessagesUpdate = (updatedMessages: Array) => { + saveChatHistory(updatedMessages); + + // if messages are empty or chatbot is open and user is not scrolling, no need to notify + if (updatedMessages.length === 0 || isChatWindowOpen && !isScrolling) { + return; + } + + // if chatbot is embedded and visible, no need to notify + if (settings.general?.embedded && isChatBotVisible(chatBodyRef.current as HTMLDivElement) || isBotTyping) { + return; + } + + const lastMessage = updatedMessages[updatedMessages.length - 1]; + // if message is sent by user or is bot typing or bot is embedded, return + if (!lastMessage || lastMessage.sender === "user") { + return; + } + + playNotificationSound(); + } return { endStreamMessage, diff --git a/src/hooks/internal/usePathsInternal.ts b/src/hooks/internal/usePathsInternal.ts index 4e948fe3..6f34efc7 100644 --- a/src/hooks/internal/usePathsInternal.ts +++ b/src/hooks/internal/usePathsInternal.ts @@ -75,11 +75,11 @@ export const usePathsInternal = () => { }, [paths, setPaths, settings]); /** - * Replaces (overwrites entirely) the current paths with the new paths. - */ + * Replaces (overwrites entirely) the current paths with the new paths. + */ const replacePaths = (newPaths: Array) => { - setPaths(newPaths); - } + setPaths(newPaths); + } return { getCurrPath, diff --git a/src/hooks/internal/useSettingsInternal.ts b/src/hooks/internal/useSettingsInternal.ts index ddeef4ef..d5a74582 100644 --- a/src/hooks/internal/useSettingsInternal.ts +++ b/src/hooks/internal/useSettingsInternal.ts @@ -9,25 +9,25 @@ export const useSettingsInternal = () => { // handles settings const { settings, setSettings } = useSettingsContext(); - /** - * Updates the settings for the chatbot. - * - * @param fields fields to update - */ - const updateSettings = (fields: object) => { - setSettings(deepClone(getCombinedConfig(fields, settings) as Settings)); - } + /** + * Updates the settings for the chatbot. + * + * @param fields fields to update + */ + const updateSettings = (fields: object) => { + setSettings(deepClone(getCombinedConfig(fields, settings) as Settings)); + } - /** - * Replaces (overwrites entirely) the current settings with the new settings. - */ - const replaceSettings = (newSettings: Settings) => { - setSettings(newSettings); - } + /** + * Replaces (overwrites entirely) the current settings with the new settings. + */ + const replaceSettings = (newSettings: Settings) => { + setSettings(newSettings); + } return { settings, replaceSettings, - updateSettings + updateSettings }; }; diff --git a/src/hooks/internal/useStylesInternal.ts b/src/hooks/internal/useStylesInternal.ts index 1293f74e..78d5a0c0 100644 --- a/src/hooks/internal/useStylesInternal.ts +++ b/src/hooks/internal/useStylesInternal.ts @@ -9,25 +9,25 @@ export const useStylesInternal = () => { // handles styles const { styles, setStyles } = useStylesContext(); - /** - * Updates the styles for the chatbot. - * - * @param fields fields to update - */ - const updateStyles = (fields: object) => { - setStyles(deepClone(getCombinedConfig(fields, styles) as Styles)); - } + /** + * Updates the styles for the chatbot. + * + * @param fields fields to update + */ + const updateStyles = (fields: object) => { + setStyles(deepClone(getCombinedConfig(fields, styles) as Styles)); + } - /** - * Replaces (overwrites entirely) the current styles with the new styles. - */ - const replaceStyles = (newStyles: Styles) => { - setStyles(newStyles); - } + /** + * Replaces (overwrites entirely) the current styles with the new styles. + */ + const replaceStyles = (newStyles: Styles) => { + setStyles(newStyles); + } return { styles, replaceStyles, - updateStyles + updateStyles }; }; diff --git a/src/hooks/internal/useToastsInternal.ts b/src/hooks/internal/useToastsInternal.ts index 46b5d142..49819ca2 100644 --- a/src/hooks/internal/useToastsInternal.ts +++ b/src/hooks/internal/useToastsInternal.ts @@ -101,7 +101,7 @@ export const useToastsInternal = () => { /** * Replaces (overwrites entirely) the current toasts with the new toasts. */ - const replaceToasts = (newToasts: Array) => { + const replaceToasts = (newToasts: Array) => { setToasts(newToasts); } From 9ed9cf9665665ea62196ac4dc98bb9c4f2b136a8 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 21 Oct 2024 02:50:53 +0800 Subject: [PATCH 09/23] fix: Fix notification sound not playing --- src/hooks/internal/useMessagesInternal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/internal/useMessagesInternal.ts b/src/hooks/internal/useMessagesInternal.ts index 966ac593..c70e6a93 100644 --- a/src/hooks/internal/useMessagesInternal.ts +++ b/src/hooks/internal/useMessagesInternal.ts @@ -304,7 +304,7 @@ export const useMessagesInternal = () => { } // if chatbot is embedded and visible, no need to notify - if (settings.general?.embedded && isChatBotVisible(chatBodyRef.current as HTMLDivElement) || isBotTyping) { + if (settings.general?.embedded && isChatBotVisible(chatBodyRef.current as HTMLDivElement)) { return; } From 082e1f4ccb6c48ee26e35d602785c31edc72ab7e Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 21 Oct 2024 02:51:31 +0800 Subject: [PATCH 10/23] chore: Remove unused import --- src/hooks/internal/useMessagesInternal.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hooks/internal/useMessagesInternal.ts b/src/hooks/internal/useMessagesInternal.ts index c70e6a93..1975cdab 100644 --- a/src/hooks/internal/useMessagesInternal.ts +++ b/src/hooks/internal/useMessagesInternal.ts @@ -29,7 +29,6 @@ export const useMessagesInternal = () => { audioToggledOn, isChatWindowOpen, isScrolling, - isBotTyping, setIsBotTyping, setUnreadCount } = useBotStatesContext(); From 41361eeab518f6baa1828ec354107991ea27a7f8 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 21 Oct 2024 02:57:06 +0800 Subject: [PATCH 11/23] refactor: Optimize scroll checks --- src/components/ChatBotBody/ChatBotBody.tsx | 43 +++++++--------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/src/components/ChatBotBody/ChatBotBody.tsx b/src/components/ChatBotBody/ChatBotBody.tsx index 5102243e..fbefdfa2 100644 --- a/src/components/ChatBotBody/ChatBotBody.tsx +++ b/src/components/ChatBotBody/ChatBotBody.tsx @@ -118,30 +118,6 @@ const ChatBotBody = ({ } }, [messages.length, isBotTyping]); - // shifts scroll position when scroll height changes - useEffect(() => { - if (!chatBodyRef.current) { - return; - } - - // used to return chat history to correct height - setChatScrollHeight(chatBodyRef.current.scrollHeight); - - if (!isScrolling) { - chatBodyRef.current.scrollTop = chatBodyRef.current.scrollHeight; - if (isChatWindowOpen) { - setUnreadCount(0); - } - } - }, [chatBodyRef.current?.scrollHeight]); - - // sets unread count to 0 if not scrolling - useEffect(() => { - if (!isScrolling) { - setUnreadCount(0); - } - }, [isScrolling]); - /** * Checks and updates whether a user is scrolling in chat window. */ @@ -149,15 +125,24 @@ const ChatBotBody = ({ if (!chatBodyRef.current) { return; } + + // used to return chat history to correct height + setChatScrollHeight(chatBodyRef.current.scrollHeight); + const { scrollTop, clientHeight, scrollHeight } = chatBodyRef.current; - setIsScrolling( - scrollTop + clientHeight < scrollHeight - (settings.chatWindow?.messagePromptOffset ?? 30) - ); + const isScrolling = scrollTop + clientHeight < scrollHeight - (settings.chatWindow?.messagePromptOffset ?? 30); + setIsScrolling(isScrolling); // workaround to ensure user never truly scrolls to bottom by introducing a 1 pixel offset // this is necessary to prevent unexpected scroll behaviors of the chat window when user reaches the bottom - if (!isScrolling && scrollTop + clientHeight >= scrollHeight - 1) { - chatBodyRef.current.scrollTop = scrollHeight - clientHeight - 1; + if (!isScrolling) { + if (scrollTop + clientHeight >= scrollHeight - 1) { + chatBodyRef.current.scrollTop = scrollHeight - clientHeight - 1; + } + + if (isChatWindowOpen) { + setUnreadCount(0); + } } }; From 4549c20500d4d66eac4c71e26c36abd6a2287cb8 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 21 Oct 2024 03:46:34 +0800 Subject: [PATCH 12/23] refactor: Minor code improvements --- .../BlockService/CheckboxProcessor.test.tsx | 2 - src/components/ChatBotBody/ChatBotBody.tsx | 38 ++++++------------- src/components/ChatBotContainer.tsx | 3 +- src/hooks/internal/useBotEffectsInternal.tsx | 11 +++++- src/hooks/internal/useChatHistoryInternal.ts | 3 +- src/hooks/internal/useMessagesInternal.ts | 13 +++++++ 6 files changed, 37 insertions(+), 33 deletions(-) diff --git a/__tests__/services/BlockService/CheckboxProcessor.test.tsx b/__tests__/services/BlockService/CheckboxProcessor.test.tsx index db52feec..96a9dfca 100644 --- a/__tests__/services/BlockService/CheckboxProcessor.test.tsx +++ b/__tests__/services/BlockService/CheckboxProcessor.test.tsx @@ -74,8 +74,6 @@ describe("processCheckboxes", () => { const expectedCheckboxes = { items: ["Item 1", "Item 2"], min: 2, max: 2 }; expect(mockParams.injectMessage).toHaveBeenCalled(); const [content] = (mockParams.injectMessage as jest.Mock).mock.calls[0]; - console.log("content.props.checkboxes:", content.props.checkboxes); - console.log("expectedCheckboxes:", expectedCheckboxes); expect(content.props.checkboxes).toMatchObject(expectedCheckboxes); }); diff --git a/src/components/ChatBotBody/ChatBotBody.tsx b/src/components/ChatBotBody/ChatBotBody.tsx index fbefdfa2..8ad08c02 100644 --- a/src/components/ChatBotBody/ChatBotBody.tsx +++ b/src/components/ChatBotBody/ChatBotBody.tsx @@ -2,6 +2,7 @@ import { Dispatch, SetStateAction, useEffect, CSSProperties, MouseEvent } from " import ChatMessagePrompt from "./ChatMessagePrompt/ChatMessagePrompt"; import ToastPrompt from "./ToastPrompt/ToastPrompt"; +import { getHistoryMessages, loadChatHistory } from "../../services/ChatHistoryService"; import { useChatWindowInternal } from "../../hooks/internal/useChatWindowInternal"; import { useBotStatesContext } from "../../context/BotStatesContext"; import { useBotRefsContext } from "../../context/BotRefsContext"; @@ -26,7 +27,6 @@ const ChatBotBody = ({ chatScrollHeight: number; setChatScrollHeight: Dispatch>; }) => { - // handles settings const { settings } = useSettingsContext(); @@ -34,7 +34,7 @@ const ChatBotBody = ({ const { styles } = useStylesContext(); // handles messages - const { messages } = useMessagesContext(); + const { messages, setMessages } = useMessagesContext(); // handles toasts const { toasts } = useToastsContext(); @@ -44,12 +44,11 @@ const ChatBotBody = ({ // handles bot states const { - isBotTyping, isLoadingChatHistory, + isBotTyping, setIsLoadingChatHistory, - isScrolling, setIsScrolling, - setUnreadCount + setUnreadCount, } = useBotStatesContext(); // handles bot refs @@ -88,35 +87,20 @@ const ChatBotBody = ({ ...styles.toastPromptContainerStyle }; - // shifts scroll position when messages are updated and when bot is typing useEffect(() => { if (isLoadingChatHistory) { - if (!chatBodyRef.current) { - return; - } - - const { scrollHeight } = chatBodyRef.current; - const scrollDifference = scrollHeight - chatScrollHeight; - chatBodyRef.current.scrollTop = chatBodyRef.current.scrollTop + scrollDifference; - setIsLoadingChatHistory(false); - return; - } - - if (settings.chatWindow?.autoJumpToBottom || !isScrolling) { - // defer update to next event loop, handles edge case where messages are sent too fast - // and the scrolling does not properly reach the bottom + loadChatHistory(settings, styles, getHistoryMessages(), setMessages); setTimeout(() => { if (!chatBodyRef.current) { return; } - - chatBodyRef.current.scrollTop = chatBodyRef.current.scrollHeight; - if (isChatWindowOpen) { - setUnreadCount(0); - } - }) + const { scrollHeight } = chatBodyRef.current; + const scrollDifference = scrollHeight - chatScrollHeight; + chatBodyRef.current.scrollTop = chatBodyRef.current.scrollTop + scrollDifference; + setIsLoadingChatHistory(false); + }, 501) } - }, [messages.length, isBotTyping]); + }, [isLoadingChatHistory]) /** * Checks and updates whether a user is scrolling in chat window. diff --git a/src/components/ChatBotContainer.tsx b/src/components/ChatBotContainer.tsx index 68f24a63..c18389d5 100644 --- a/src/components/ChatBotContainer.tsx +++ b/src/components/ChatBotContainer.tsx @@ -48,7 +48,8 @@ const ChatBotContainer = ({ const { inputRef } = useBotRefsContext(); // handles chat window - const { chatScrollHeight, + const { + chatScrollHeight, setChatScrollHeight, viewportHeight, viewportWidth, diff --git a/src/hooks/internal/useBotEffectsInternal.tsx b/src/hooks/internal/useBotEffectsInternal.tsx index 28349799..c743c911 100644 --- a/src/hooks/internal/useBotEffectsInternal.tsx +++ b/src/hooks/internal/useBotEffectsInternal.tsx @@ -49,7 +49,9 @@ export const useBotEffectsInternal = () => { // handles bot states const { + isBotTyping, isChatWindowOpen, + isScrolling, hasFlowStarted, setIsChatWindowOpen, setTextAreaDisabled, @@ -62,7 +64,7 @@ export const useBotEffectsInternal = () => { } = useBotStatesContext(); // handles bot refs - const { flowRef, streamMessageMap, paramsInputRef, keepVoiceOnRef } = useBotRefsContext(); + const { chatBodyRef, flowRef, streamMessageMap, paramsInputRef, keepVoiceOnRef } = useBotRefsContext(); const flow = flowRef.current as Flow; // handles chat window @@ -112,6 +114,13 @@ export const useBotEffectsInternal = () => { }, 1) }, []) + // scrolls to bottom if bot is typing and user is not scrolling + useEffect(() => { + if (!isScrolling && chatBodyRef?.current) { + chatBodyRef.current.scrollTop = chatBodyRef.current.scrollHeight; + } + }, [isBotTyping]) + // renders chat history button if enabled and triggers update if chat history configurations change useEffect(() => { if (settings.chatHistory?.disabled) { diff --git a/src/hooks/internal/useChatHistoryInternal.ts b/src/hooks/internal/useChatHistoryInternal.ts index da3022b9..9a8ed4fd 100644 --- a/src/hooks/internal/useChatHistoryInternal.ts +++ b/src/hooks/internal/useChatHistoryInternal.ts @@ -1,7 +1,7 @@ import { useCallback } from "react"; +import { getHistoryMessages } from "../../services/ChatHistoryService"; import { useRcbEventInternal } from "./useRcbEventInternal"; -import { getHistoryMessages, loadChatHistory } from "../../services/ChatHistoryService"; import { useMessagesContext } from "../../context/MessagesContext"; import { useSettingsContext } from "../../context/SettingsContext"; import { useStylesContext } from "../../context/StylesContext"; @@ -49,7 +49,6 @@ export const useChatHistoryInternal = () => { } } setIsLoadingChatHistory(true); - loadChatHistory(settings, styles, chatHistory, setMessages); }, [settings, styles, setMessages]); return { isLoadingChatHistory, setIsLoadingChatHistory, showChatHistory }; diff --git a/src/hooks/internal/useMessagesInternal.ts b/src/hooks/internal/useMessagesInternal.ts index 1975cdab..0452ad1a 100644 --- a/src/hooks/internal/useMessagesInternal.ts +++ b/src/hooks/internal/useMessagesInternal.ts @@ -314,6 +314,19 @@ export const useMessagesInternal = () => { } playNotificationSound(); + + // if auto scroll to bottom, then scroll to bottom + if (settings.chatWindow?.autoJumpToBottom) { + // defer update to next event loop, handles edge case where messages are sent too fast + // and the scrolling does not properly reach the bottom + setTimeout(() => { + if (!chatBodyRef.current) { + return; + } + + chatBodyRef.current.scrollTop = chatBodyRef.current.scrollHeight; + }) + } } return { From 7e6959072fd3ec9d9fe40d68fa564f3594cb4879 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 21 Oct 2024 03:51:36 +0800 Subject: [PATCH 13/23] refactor: Optimize button creation --- src/hooks/internal/useButtonsInternal.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/hooks/internal/useButtonsInternal.ts b/src/hooks/internal/useButtonsInternal.ts index 02ac31ee..3d2ed817 100644 --- a/src/hooks/internal/useButtonsInternal.ts +++ b/src/hooks/internal/useButtonsInternal.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useMemo } from "react"; import { createAudioButton, @@ -20,11 +20,6 @@ export const useButtonInternal = () => { // handles settings const { settings } = useSettingsContext(); - // buttons to show in header, chat input and footer - const [headerButtons, setHeaderButtons] = useState>([]); - const [chatInputButtons, setChatInputButtons] = useState>([]); - const [footerButtons, setFooterButtons] = useState>([]); - // handles the rendering of buttons const staticButtonComponentMap = useMemo(() => ({ [Button.CLOSE_CHAT_BUTTON]: () => createCloseChatButton(), @@ -36,13 +31,15 @@ export const useButtonInternal = () => { [Button.VOICE_MESSAGE_BUTTON]: () => createVoiceButton() }), []); - // sets buttons to be shown - useEffect(() => { - const buttonConfig = getButtonConfig(settings, staticButtonComponentMap); - setHeaderButtons(buttonConfig.header); - setChatInputButtons(buttonConfig.chatInput); - setFooterButtons(buttonConfig.footer); + // computes button configurations whenever settings or map changes + const { header, chatInput, footer } = useMemo(() => { + return getButtonConfig(settings, staticButtonComponentMap); }, [settings, staticButtonComponentMap]); - return {headerButtons, chatInputButtons, footerButtons}; + // memoizes creation of jsx elements + const headerButtons = useMemo(() => header, [header]); + const chatInputButtons = useMemo(() => chatInput, [chatInput]); + const footerButtons = useMemo(() => footer, [footer]); + + return { headerButtons, chatInputButtons, footerButtons }; }; From ef61440a64d3b02b09b4a3c5ea3b311d403b0089 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 21 Oct 2024 03:56:22 +0800 Subject: [PATCH 14/23] refactor: Optimize voice button --- .../Buttons/VoiceButton/VoiceButton.tsx | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/components/Buttons/VoiceButton/VoiceButton.tsx b/src/components/Buttons/VoiceButton/VoiceButton.tsx index e611e012..dab4bb42 100644 --- a/src/components/Buttons/VoiceButton/VoiceButton.tsx +++ b/src/components/Buttons/VoiceButton/VoiceButton.tsx @@ -1,4 +1,4 @@ -import { useEffect, MouseEvent, useState, useRef } from "react"; +import { useEffect, MouseEvent, useRef } from "react"; import MediaDisplay from "../../ChatBotBody/MediaDisplay/MediaDisplay"; import { startVoiceRecording, stopVoiceRecording } from "../../../services/VoiceService"; @@ -42,17 +42,6 @@ const VoiceButton = () => { // tracks audio chunk (if voice is sent as audio) const audioChunksRef = useRef([]); - // serves as a workaround (together with useEffect hook) for sending voice input, can consider a better approach - const [voiceInputTrigger, setVoiceInputTrigger] = useState(false); - useEffect(() => { - if (settings.voice?.sendAsAudio) { - handleSendAsAudio(); - audioChunksRef.current = []; - } else { - handleSubmitText(); - } - }, [voiceInputTrigger]) - // handles starting and stopping of voice recording on toggle useEffect(() => { if (voiceToggledOn) { @@ -82,7 +71,12 @@ const VoiceButton = () => { * Handles submission of user voice input. */ const triggerSendVoiceInput = () => { - setVoiceInputTrigger(prev => !prev); + if (settings.voice?.sendAsAudio) { + handleSendAsAudio(); + audioChunksRef.current = []; + } else { + handleSubmitText(); + } } /* From 00b1f91743b78c0bad17227b78f056d05d856961 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 21 Oct 2024 04:08:41 +0800 Subject: [PATCH 15/23] fix: Fix message scrolling --- src/hooks/internal/useMessagesInternal.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/hooks/internal/useMessagesInternal.ts b/src/hooks/internal/useMessagesInternal.ts index 0452ad1a..a3b9c1cf 100644 --- a/src/hooks/internal/useMessagesInternal.ts +++ b/src/hooks/internal/useMessagesInternal.ts @@ -292,31 +292,37 @@ export const useMessagesInternal = () => { } /** - * Handles post messages updates such as saving chat history and playing notification sound. + * Handles post messages updates such as saving chat history, scrolling to bottom + * and playing notification sound. */ const handlePostMessagesUpdate = (updatedMessages: Array) => { saveChatHistory(updatedMessages); + // tracks if notification should be played + let shouldNotify = true; + // if messages are empty or chatbot is open and user is not scrolling, no need to notify if (updatedMessages.length === 0 || isChatWindowOpen && !isScrolling) { - return; + shouldNotify = false; } // if chatbot is embedded and visible, no need to notify if (settings.general?.embedded && isChatBotVisible(chatBodyRef.current as HTMLDivElement)) { - return; + shouldNotify = false; } const lastMessage = updatedMessages[updatedMessages.length - 1]; // if message is sent by user or is bot typing or bot is embedded, return if (!lastMessage || lastMessage.sender === "user") { - return; + shouldNotify = false; } - playNotificationSound(); + if (shouldNotify) { + playNotificationSound(); + } // if auto scroll to bottom, then scroll to bottom - if (settings.chatWindow?.autoJumpToBottom) { + if (settings.chatWindow?.autoJumpToBottom || !isScrolling) { // defer update to next event loop, handles edge case where messages are sent too fast // and the scrolling does not properly reach the bottom setTimeout(() => { From 2220cd6a66050fd3a2a130bc4be264683cd0ddf0 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 21 Oct 2024 04:15:45 +0800 Subject: [PATCH 16/23] fix: Fix voice input scrolling --- src/hooks/internal/useMessagesInternal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/internal/useMessagesInternal.ts b/src/hooks/internal/useMessagesInternal.ts index a3b9c1cf..986c9da4 100644 --- a/src/hooks/internal/useMessagesInternal.ts +++ b/src/hooks/internal/useMessagesInternal.ts @@ -321,7 +321,7 @@ export const useMessagesInternal = () => { playNotificationSound(); } - // if auto scroll to bottom, then scroll to bottom + // if auto scroll enabled or is not scrolling, then scroll to bottom if (settings.chatWindow?.autoJumpToBottom || !isScrolling) { // defer update to next event loop, handles edge case where messages are sent too fast // and the scrolling does not properly reach the bottom @@ -331,7 +331,7 @@ export const useMessagesInternal = () => { } chatBodyRef.current.scrollTop = chatBodyRef.current.scrollHeight; - }) + }, 1) } } From 8d8c8c7518c3e5ced1dfd285557a775aff1da472 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 21 Oct 2024 23:17:01 +0800 Subject: [PATCH 17/23] fix: Minor bug fixes --- .../internal/useChatHistoryInternal.test.ts | 3 +++ .../Buttons/VoiceButton/VoiceButton.tsx | 20 +++++++++------ src/components/ChatBotBody/ChatBotBody.tsx | 25 ++----------------- src/components/ChatBotContainer.tsx | 3 +-- src/hooks/internal/useChatHistoryInternal.ts | 13 +++++++++- src/services/ChatHistoryService.tsx | 19 +++++++++++--- 6 files changed, 47 insertions(+), 36 deletions(-) diff --git a/__tests__/hooks/internal/useChatHistoryInternal.test.ts b/__tests__/hooks/internal/useChatHistoryInternal.test.ts index 3bace45c..de7beff0 100644 --- a/__tests__/hooks/internal/useChatHistoryInternal.test.ts +++ b/__tests__/hooks/internal/useChatHistoryInternal.test.ts @@ -90,6 +90,9 @@ describe("useChatHistoryInternal Hook", () => { expect.any(Object), initialChatHistory, expect.any(Function), + expect.any(Object), + expect.any(Number), + expect.any(Function), ); // checks if history is being loaded diff --git a/src/components/Buttons/VoiceButton/VoiceButton.tsx b/src/components/Buttons/VoiceButton/VoiceButton.tsx index dab4bb42..376ebd21 100644 --- a/src/components/Buttons/VoiceButton/VoiceButton.tsx +++ b/src/components/Buttons/VoiceButton/VoiceButton.tsx @@ -1,4 +1,4 @@ -import { useEffect, MouseEvent, useRef } from "react"; +import { useEffect, MouseEvent, useRef, useState } from "react"; import MediaDisplay from "../../ChatBotBody/MediaDisplay/MediaDisplay"; import { startVoiceRecording, stopVoiceRecording } from "../../../services/VoiceService"; @@ -42,6 +42,17 @@ const VoiceButton = () => { // tracks audio chunk (if voice is sent as audio) const audioChunksRef = useRef([]); + // serves as a workaround (together with useEffect hook) for sending voice input, can consider a better approach + const [voiceInputTrigger, setVoiceInputTrigger] = useState(false); + useEffect(() => { + if (settings.voice?.sendAsAudio) { + handleSendAsAudio(); + audioChunksRef.current = []; + } else { + handleSubmitText(); + } + }, [voiceInputTrigger]) + // handles starting and stopping of voice recording on toggle useEffect(() => { if (voiceToggledOn) { @@ -71,12 +82,7 @@ const VoiceButton = () => { * Handles submission of user voice input. */ const triggerSendVoiceInput = () => { - if (settings.voice?.sendAsAudio) { - handleSendAsAudio(); - audioChunksRef.current = []; - } else { - handleSubmitText(); - } + setVoiceInputTrigger(prev => !prev); } /* diff --git a/src/components/ChatBotBody/ChatBotBody.tsx b/src/components/ChatBotBody/ChatBotBody.tsx index 8ad08c02..40ff9bf7 100644 --- a/src/components/ChatBotBody/ChatBotBody.tsx +++ b/src/components/ChatBotBody/ChatBotBody.tsx @@ -1,8 +1,7 @@ -import { Dispatch, SetStateAction, useEffect, CSSProperties, MouseEvent } from "react"; +import { Dispatch, SetStateAction, CSSProperties, MouseEvent } from "react"; import ChatMessagePrompt from "./ChatMessagePrompt/ChatMessagePrompt"; import ToastPrompt from "./ToastPrompt/ToastPrompt"; -import { getHistoryMessages, loadChatHistory } from "../../services/ChatHistoryService"; import { useChatWindowInternal } from "../../hooks/internal/useChatWindowInternal"; import { useBotStatesContext } from "../../context/BotStatesContext"; import { useBotRefsContext } from "../../context/BotRefsContext"; @@ -17,14 +16,11 @@ import "./ChatBotBody.css"; /** * Contains chat messages between the user and bot. * - * @param chatScrollHeight number representing chat window scroll height * @param setChatScrollHeight setter for tracking chat window scroll height */ const ChatBotBody = ({ - chatScrollHeight, setChatScrollHeight, }: { - chatScrollHeight: number; setChatScrollHeight: Dispatch>; }) => { // handles settings @@ -34,7 +30,7 @@ const ChatBotBody = ({ const { styles } = useStylesContext(); // handles messages - const { messages, setMessages } = useMessagesContext(); + const { messages } = useMessagesContext(); // handles toasts const { toasts } = useToastsContext(); @@ -44,9 +40,7 @@ const ChatBotBody = ({ // handles bot states const { - isLoadingChatHistory, isBotTyping, - setIsLoadingChatHistory, setIsScrolling, setUnreadCount, } = useBotStatesContext(); @@ -87,21 +81,6 @@ const ChatBotBody = ({ ...styles.toastPromptContainerStyle }; - useEffect(() => { - if (isLoadingChatHistory) { - loadChatHistory(settings, styles, getHistoryMessages(), setMessages); - setTimeout(() => { - if (!chatBodyRef.current) { - return; - } - const { scrollHeight } = chatBodyRef.current; - const scrollDifference = scrollHeight - chatScrollHeight; - chatBodyRef.current.scrollTop = chatBodyRef.current.scrollTop + scrollDifference; - setIsLoadingChatHistory(false); - }, 501) - } - }, [isLoadingChatHistory]) - /** * Checks and updates whether a user is scrolling in chat window. */ diff --git a/src/components/ChatBotContainer.tsx b/src/components/ChatBotContainer.tsx index c18389d5..eaa1f9e7 100644 --- a/src/components/ChatBotContainer.tsx +++ b/src/components/ChatBotContainer.tsx @@ -49,7 +49,6 @@ const ChatBotContainer = ({ // handles chat window const { - chatScrollHeight, setChatScrollHeight, viewportHeight, viewportWidth, @@ -147,7 +146,7 @@ const ChatBotContainer = ({ }
{settings.general?.showHeader && } - + {settings.general?.showInputRow && } {settings.general?.showFooter && }
diff --git a/src/hooks/internal/useChatHistoryInternal.ts b/src/hooks/internal/useChatHistoryInternal.ts index 9a8ed4fd..bdebf225 100644 --- a/src/hooks/internal/useChatHistoryInternal.ts +++ b/src/hooks/internal/useChatHistoryInternal.ts @@ -1,7 +1,9 @@ import { useCallback } from "react"; -import { getHistoryMessages } from "../../services/ChatHistoryService"; +import { getHistoryMessages, loadChatHistory } from "../../services/ChatHistoryService"; import { useRcbEventInternal } from "./useRcbEventInternal"; +import { useChatWindowInternal } from "./useChatWindowInternal"; +import { useBotRefsContext } from "../../context/BotRefsContext"; import { useMessagesContext } from "../../context/MessagesContext"; import { useSettingsContext } from "../../context/SettingsContext"; import { useStylesContext } from "../../context/StylesContext"; @@ -27,9 +29,15 @@ export const useChatHistoryInternal = () => { setIsLoadingChatHistory, } = useBotStatesContext(); + // handles bot refs + const { chatBodyRef } = useBotRefsContext(); + // handles rcb events const { callRcbEvent } = useRcbEventInternal(); + // handles chat window + const { chatScrollHeight } = useChatWindowInternal(); + /** * Loads and shows chat history in the chat window. * @@ -49,6 +57,9 @@ export const useChatHistoryInternal = () => { } } setIsLoadingChatHistory(true); + loadChatHistory(settings, styles, chatHistory, setMessages, + chatBodyRef, chatScrollHeight, setIsLoadingChatHistory + ); }, [settings, styles, setMessages]); return { isLoadingChatHistory, setIsLoadingChatHistory, showChatHistory }; diff --git a/src/services/ChatHistoryService.tsx b/src/services/ChatHistoryService.tsx index 6c39834e..454ac59b 100644 --- a/src/services/ChatHistoryService.tsx +++ b/src/services/ChatHistoryService.tsx @@ -124,11 +124,13 @@ const parseMessageToString = (message: Message) => { * @param styles styles provided to the bot * @param chatHistory chat history to show * @param setMessages setter for updating messages - * @param prevTextAreaDisabled boolean indicating if text area was previously disabled - * @param setTextAreaDisabled setter for enabling/disabling user text area + * @param chatBodyRef reference to the chat body + * @param chatScrollHeight current chat scroll height + * @param setIsLoadingChatHistory setter for whether chat history is loading */ const loadChatHistory = (settings: Settings, styles: Styles, chatHistory: Message[], - setMessages: Dispatch>) => { + setMessages: Dispatch>, chatBodyRef: React.RefObject, + chatScrollHeight: number, setIsLoadingChatHistory: Dispatch>) => { historyLoaded = true; if (chatHistory != null) { @@ -160,6 +162,17 @@ const loadChatHistory = (settings: Settings, styles: Styles, chatHistory: Messag return [...parsedMessages, lineBreakMessage, ...prevMessages]; }); }, 500) + + // slight delay afterwards to maintain scroll position + setTimeout(() => { + if (!chatBodyRef.current) { + return; + } + const { scrollHeight } = chatBodyRef.current; + const scrollDifference = scrollHeight - chatScrollHeight; + chatBodyRef.current.scrollTop = chatBodyRef.current.scrollTop + scrollDifference; + setIsLoadingChatHistory(false); + }, 510) } catch { // remove chat history on error (to address corrupted storage values) localStorage.removeItem(settings.chatHistory?.storageKey as string); From 2d3053b4b62706288d7d3143275c209912e2f291 Mon Sep 17 00:00:00 2001 From: Akanni Modupe Adegoke Date: Mon, 21 Oct 2024 16:36:28 +0100 Subject: [PATCH 18/23] test: add unit tests for useMessagesInternal hook (#234) --- .../internal/useMessagesInternal.test.ts | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 __tests__/hooks/internal/useMessagesInternal.test.ts diff --git a/__tests__/hooks/internal/useMessagesInternal.test.ts b/__tests__/hooks/internal/useMessagesInternal.test.ts new file mode 100644 index 00000000..51fa7585 --- /dev/null +++ b/__tests__/hooks/internal/useMessagesInternal.test.ts @@ -0,0 +1,122 @@ +import { renderHook } from "@testing-library/react"; +import { act } from "react"; +import { useMessagesInternal } from "../../../src/hooks/internal/useMessagesInternal"; +import { useSettingsContext } from "../../../src/context/SettingsContext"; +import { useMessagesContext } from "../../../src/context/MessagesContext"; +import { useBotStatesContext } from "../../../src/context/BotStatesContext"; +import { useBotRefsContext } from "../../../src/context/BotRefsContext"; +import { useRcbEventInternal } from "../../../src/hooks/internal/useRcbEventInternal"; +import { Message } from "../../../src/types/Message"; + +jest.mock("../../../src/context/SettingsContext"); +jest.mock("../../../src/context/MessagesContext"); +jest.mock("../../../src/context/BotStatesContext"); +jest.mock("../../../src/context/BotRefsContext"); +jest.mock("../../../src/hooks/internal/useRcbEventInternal"); +jest.mock("../../../src/services/AudioService"); +jest.mock("../../../src/services/ChatHistoryService"); + +describe("useMessagesInternal", () => { + const mockSetMessages = jest.fn(); + const mockSetIsBotTyping = jest.fn(); + const mockSetUnreadCount = jest.fn(); + const mockCallRcbEvent = jest.fn(); + const mockStreamMessageMap = { current: new Map() }; + const mockMessages: Message[] = []; + + beforeEach(() => { + jest.clearAllMocks(); + (useSettingsContext as jest.Mock).mockReturnValue({ + settings: { + botBubble: { dangerouslySetInnerHtml: false, simStream: false }, + userBubble: { dangerouslySetInnerHtml: false, simStream: false }, + event: {}, + }, + }); + (useMessagesContext as jest.Mock).mockReturnValue({ + messages: mockMessages, + setMessages: mockSetMessages, + }); + (useBotStatesContext as jest.Mock).mockReturnValue({ + audioToggledOn: false, + isChatWindowOpen: true, + setIsBotTyping: mockSetIsBotTyping, + setUnreadCount: mockSetUnreadCount, + }); + (useBotRefsContext as jest.Mock).mockReturnValue({ + streamMessageMap: mockStreamMessageMap, + }); + (useRcbEventInternal as jest.Mock).mockReturnValue({ + callRcbEvent: mockCallRcbEvent, + }); + }); + + it("should return expected functions and values", () => { + const { result } = renderHook(() => useMessagesInternal()); + + expect(result.current).toHaveProperty("endStreamMessage"); + expect(result.current).toHaveProperty("injectMessage"); + expect(result.current).toHaveProperty("removeMessage"); + expect(result.current).toHaveProperty("streamMessage"); + expect(result.current).toHaveProperty("messages"); + expect(result.current).toHaveProperty("setMessages"); + }); + + it("should inject a message correctly", async () => { + const { result } = renderHook(() => useMessagesInternal()); + + await act(async () => { + const messageId = await result.current.injectMessage("Test message", "bot"); + expect(messageId).toBeTruthy(); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + expect(mockSetUnreadCount).toHaveBeenCalledWith(expect.any(Function)); + }); + + it("should remove a message correctly", async () => { + const mockMessageId = "test-id"; + const mockMessage: Message = { id: mockMessageId, content: "Test", sender: "bot", type: "text", + timestamp: String(Date.now()) }; + (useMessagesContext as jest.Mock).mockReturnValue({ + messages: [mockMessage], + setMessages: mockSetMessages, + }); + + const { result } = renderHook(() => useMessagesInternal()); + + await act(async () => { + const removedId = await result.current.removeMessage(mockMessageId); + expect(removedId).toBe(mockMessageId); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + expect(mockSetUnreadCount).toHaveBeenCalledWith(expect.any(Function)); + }); + + it("should stream a message correctly", async () => { + const { result } = renderHook(() => useMessagesInternal()); + + await act(async () => { + const messageId = await result.current.streamMessage("Test stream", "bot"); + expect(messageId).toBeTruthy(); + }); + + expect(mockSetMessages).toHaveBeenCalled(); + expect(mockSetUnreadCount).toHaveBeenCalledWith(expect.any(Function)); + expect(mockStreamMessageMap.current.has("bot")).toBeTruthy(); + }); + + it("should end stream message correctly", async () => { + mockStreamMessageMap.current.set("bot", "test-id"); + const { result } = renderHook(() => useMessagesInternal()); + + await act(async () => { + const success = await result.current.endStreamMessage("bot"); + expect(success).toBeTruthy(); + }); + + expect(mockStreamMessageMap.current.has("bot")).toBeFalsy(); + }); + +}); \ No newline at end of file From 66ce0803dd488f1f6523150f16372460a72dcbf6 Mon Sep 17 00:00:00 2001 From: Rahul RK <47377566+rahulrk-dev@users.noreply.github.com> Date: Mon, 21 Oct 2024 21:08:23 +0530 Subject: [PATCH 19/23] chore: add test cases for PathsContext (#238) --- __tests__/context/PathsContext.test.tsx | 84 +++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 __tests__/context/PathsContext.test.tsx diff --git a/__tests__/context/PathsContext.test.tsx b/__tests__/context/PathsContext.test.tsx new file mode 100644 index 00000000..269603a1 --- /dev/null +++ b/__tests__/context/PathsContext.test.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import { expect } from '@jest/globals' +import { act, render, screen } from '@testing-library/react' +import '@testing-library/jest-dom/jest-globals' + +import { usePathsContext, PathsProvider } from '../../src/context/PathsContext' + +const TestComponent = () => { + const { paths, setPaths } = usePathsContext() + + const handleAddPath = () => { + setPaths([...paths, '/path/to/1']) + } + + const handleClearPaths = () => { + setPaths([]) + } + + return ( +
+

Paths Count: {paths.length}

+ + + +
+ ) +} + +describe('PathsContext', () => { + it('provides the correct default values', () => { + render( + + + + ) + + expect(screen.getByTestId('pathsCount')).toHaveTextContent(`Paths Count: 0`) + }) + + it('allows adding paths in the context', () => { + render( + + + + ) + + const addPathBtn = screen.getByText('Add Path') + + act(() => { + addPathBtn.click() + }) + + expect(screen.getByTestId('pathsCount')).toHaveTextContent(`Paths Count: 1`) + + act(() => { + addPathBtn.click() + }) + + expect(screen.getByTestId('pathsCount')).toHaveTextContent(`Paths Count: 2`) + }) + + it('allows updating paths in the context', () => { + render( + + + + ) + + const clearPathsBtn = screen.getByText('Clear Paths') + const addPathBtn = screen.getByText('Add Path') + + act(() => { + clearPathsBtn.click() + }) + + expect(screen.getByTestId('pathsCount')).toHaveTextContent(`Paths Count: 0`) + + act(() => { + addPathBtn.click() + }) + + expect(screen.getByTestId('pathsCount')).toHaveTextContent(`Paths Count: 1`) + }) +}) From 5fea96ec516466d8946fd8afff843c09a5541ea8 Mon Sep 17 00:00:00 2001 From: Nandini Bajaj <90183734+Nandinibajaj16@users.noreply.github.com> Date: Mon, 21 Oct 2024 21:15:23 +0530 Subject: [PATCH 20/23] test: adds unit tests for the hook useIsDesktopInternal (#240) --- .../internal/useIsDesktopInternal.test.ts | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 __tests__/hooks/internal/useIsDesktopInternal.test.ts diff --git a/__tests__/hooks/internal/useIsDesktopInternal.test.ts b/__tests__/hooks/internal/useIsDesktopInternal.test.ts new file mode 100644 index 00000000..9f9b0e0c --- /dev/null +++ b/__tests__/hooks/internal/useIsDesktopInternal.test.ts @@ -0,0 +1,76 @@ +import { renderHook } from '@testing-library/react'; +import { useIsDesktopInternal} from "../../../src/hooks/internal/useIsDesktopInternal"; + +const originalNavigator = window.navigator; +const originalInnerWidth = window.innerWidth; + +const mockWindowProperty = (property: string, value: any) => { + Object.defineProperty(window, property, { + configurable: true, + writable: true, + value, + }); +}; + +describe('useIsDesktopInternal', () => { + afterEach(() => { + Object.defineProperty(window, 'navigator', { + configurable: true, + writable: true, + value: originalNavigator, + }); + Object.defineProperty(window, 'innerWidth', { + configurable: true, + writable: true, + value: originalInnerWidth, + }); + }); + + it('should return false when running on server-side (no window object)', () => { + mockWindowProperty('navigator', undefined); + mockWindowProperty('innerWidth', 0); + + const { result } = renderHook(() => useIsDesktopInternal()); + expect(result.current).toBe(false); + }); + + it('should return false for a mobile user agent', () => { + mockWindowProperty('navigator', { + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15A372 Safari/604.1', + }); + mockWindowProperty('innerWidth', 800); + + const { result } = renderHook(() => useIsDesktopInternal()); + expect(result.current).toBe(false); + }); + + it('should return true for a desktop user agent and wide screen', () => { + mockWindowProperty('navigator', { + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + }); + mockWindowProperty('innerWidth', 1024); + + const { result } = renderHook(() => useIsDesktopInternal()); + expect(result.current).toBe(true); + }); + + it('should return false for a narrow screen, even on a desktop user agent', () => { + mockWindowProperty('navigator', { + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + }); + mockWindowProperty('innerWidth', 500); + + const { result } = renderHook(() => useIsDesktopInternal()); + expect(result.current).toBe(false); + }); + + it('should return true if user agent is not mobile and window width is exactly 768', () => { + mockWindowProperty('navigator', { + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + }); + mockWindowProperty('innerWidth', 768); + + const { result } = renderHook(() => useIsDesktopInternal()); + expect(result.current).toBe(true); + }); +}); From 953f42f1931a66866fd9e7bdd2552dff3909019b Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 21 Oct 2024 23:44:36 +0800 Subject: [PATCH 21/23] refactor: Update plugin type --- src/types/Plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/Plugin.ts b/src/types/Plugin.ts index d20d0695..602a21a3 100644 --- a/src/types/Plugin.ts +++ b/src/types/Plugin.ts @@ -1,4 +1,4 @@ /** * Defines a plugin type. */ -export type Plugin = (...args: unknown[]) => unknown; \ No newline at end of file +export type Plugin = (...args: unknown[]) => (...hookArgs: unknown[]) => string; \ No newline at end of file From 512c200480986fb30c69b44fc798c3dc3cc289c9 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Mon, 21 Oct 2024 23:50:11 +0800 Subject: [PATCH 22/23] docs: Update changelog and version --- CHANGELOG.md | 14 ++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 098d8f1b..aa661192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # CHANGELOG.md +## v2.0.0-beta.20 (21-10-2024) + +**Fixed:** +- Fixed an issue with the default chatbot footer icon +- Improved checks for desktop/mobile devices +- Reduced unnecessary re-renders (minor optimizations) +- Properly fixed chatbot svg icon on mobile + +**Added:** +- Added new `replaceSettings`, `replaceStyles`, `replaceMessages`, `replacePaths` and `replaceToasts` utility functions to their respective hooks (replaces their respective state setters) + +**Note:** +Hooks no longer directly expose state setters (not a great practice, and hinders optimizations that can be done within the library itself). The new functions serve as a drop-in replacement for the state setters. + ## v2.0.0-beta.19 (18-10-2024) **Fixed:** diff --git a/package-lock.json b/package-lock.json index 5cf7a55a..f4217ca4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-chatbotify", - "version": "2.0.0-beta.19", + "version": "2.0.0-beta.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-chatbotify", - "version": "2.0.0-beta.19", + "version": "2.0.0-beta.20", "license": "MIT", "devDependencies": { "@testing-library/jest-dom": "^6.5.0", diff --git a/package.json b/package.json index 78df543e..6e00c095 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "files": [ "./dist" ], - "version": "2.0.0-beta.19", + "version": "2.0.0-beta.20", "description": "A modern React library for creating flexible and extensible chatbots.", "type": "module", "main": "./dist/index.cjs", From aaac2b2f36d5078534666acf6aed5f75e892d267 Mon Sep 17 00:00:00 2001 From: tjtanjin Date: Tue, 22 Oct 2024 00:14:49 +0800 Subject: [PATCH 23/23] fix: Fix unit test for messages hook --- __tests__/hooks/internal/useMessagesInternal.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/hooks/internal/useMessagesInternal.test.ts b/__tests__/hooks/internal/useMessagesInternal.test.ts index 51fa7585..c233b25b 100644 --- a/__tests__/hooks/internal/useMessagesInternal.test.ts +++ b/__tests__/hooks/internal/useMessagesInternal.test.ts @@ -59,7 +59,7 @@ describe("useMessagesInternal", () => { expect(result.current).toHaveProperty("removeMessage"); expect(result.current).toHaveProperty("streamMessage"); expect(result.current).toHaveProperty("messages"); - expect(result.current).toHaveProperty("setMessages"); + expect(result.current).toHaveProperty("replaceMessages"); }); it("should inject a message correctly", async () => {