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/__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`
+ )
+ })
+})
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`)
+ })
+})
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/__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);
+ });
+});
diff --git a/__tests__/hooks/internal/useMessagesInternal.test.ts b/__tests__/hooks/internal/useMessagesInternal.test.ts
new file mode 100644
index 00000000..c233b25b
--- /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("replaceMessages");
+ });
+
+ 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
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/__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/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",
diff --git a/src/components/Buttons/VoiceButton/VoiceButton.tsx b/src/components/Buttons/VoiceButton/VoiceButton.tsx
index e611e012..376ebd21 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, useState } from "react";
import MediaDisplay from "../../ChatBotBody/MediaDisplay/MediaDisplay";
import { startVoiceRecording, stopVoiceRecording } from "../../../services/VoiceService";
@@ -52,7 +52,7 @@ const VoiceButton = () => {
handleSubmitText();
}
}, [voiceInputTrigger])
-
+
// handles starting and stopping of voice recording on toggle
useEffect(() => {
if (voiceToggledOn) {
diff --git a/src/components/ChatBotBody/ChatBotBody.tsx b/src/components/ChatBotBody/ChatBotBody.tsx
index 5102243e..40ff9bf7 100644
--- a/src/components/ChatBotBody/ChatBotBody.tsx
+++ b/src/components/ChatBotBody/ChatBotBody.tsx
@@ -1,4 +1,4 @@
-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";
@@ -16,17 +16,13 @@ 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
const { settings } = useSettingsContext();
@@ -45,11 +41,8 @@ const ChatBotBody = ({
// handles bot states
const {
isBotTyping,
- isLoadingChatHistory,
- setIsLoadingChatHistory,
- isScrolling,
setIsScrolling,
- setUnreadCount
+ setUnreadCount,
} = useBotStatesContext();
// handles bot refs
@@ -88,60 +81,6 @@ 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
- setTimeout(() => {
- if (!chatBodyRef.current) {
- return;
- }
-
- chatBodyRef.current.scrollTop = chatBodyRef.current.scrollHeight;
- if (isChatWindowOpen) {
- setUnreadCount(0);
- }
- })
- }
- }, [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 +88,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);
+ }
}
};
diff --git a/src/components/ChatBotContainer.tsx b/src/components/ChatBotContainer.tsx
index b193172b..eaa1f9e7 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";
@@ -48,7 +48,7 @@ const ChatBotContainer = ({
const { inputRef } = useBotRefsContext();
// handles chat window
- const { chatScrollHeight,
+ const {
setChatScrollHeight,
viewportHeight,
viewportWidth,
@@ -59,7 +59,7 @@ const ChatBotContainer = ({
const { headerButtons, chatInputButtons, footerButtons } = useButtonInternal();
// loads all use effects
- useBotEffectInternal();
+ useBotEffectsInternal();
/**
* Retrieves class name for window state.
@@ -146,7 +146,7 @@ const ChatBotContainer = ({
}
{settings.general?.showHeader && }
-
+
{settings.general?.showInputRow && }
{settings.general?.showFooter && }
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
diff --git a/src/hooks/internal/useBotEffectsInternal.tsx b/src/hooks/internal/useBotEffectsInternal.tsx
index 7e440e83..c743c911 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";
@@ -25,7 +24,7 @@ import { Params } from "../../types/Params";
/**
* Internal custom hook for common use effects.
*/
-export const useBotEffectInternal = () => {
+export const useBotEffectsInternal = () => {
// handles platform
const isDesktop = useIsDesktopInternal();
@@ -39,7 +38,7 @@ export const useBotEffectInternal = () => {
removeMessage,
streamMessage,
messages,
- setMessages
+ replaceMessages,
} = useMessagesInternal();
// handles paths
@@ -50,10 +49,9 @@ export const useBotEffectInternal = () => {
// handles bot states
const {
- isChatWindowOpen,
isBotTyping,
+ isChatWindowOpen,
isScrolling,
- timeoutId,
hasFlowStarted,
setIsChatWindowOpen,
setTextAreaDisabled,
@@ -66,14 +64,14 @@ export const useBotEffectInternal = () => {
} = useBotStatesContext();
// handles bot refs
- const { flowRef, chatBodyRef, streamMessageMap, paramsInputRef, keepVoiceOnRef } = useBotRefsContext();
+ const { chatBodyRef, flowRef, streamMessageMap, paramsInputRef, keepVoiceOnRef } = useBotRefsContext();
const flow = flowRef.current as Flow;
// handles chat window
const { viewportHeight, setViewportHeight, setViewportWidth, openChat } = useChatWindowInternal();
// handles notifications
- const { playNotificationSound, setUnreadCount, setUpNotifications } = useNotificationInternal();
+ const { setUpNotifications } = useNotificationInternal();
// handles user first interaction
const { handleFirstInteraction } = useFirstInteractionInternal();
@@ -103,13 +101,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);
@@ -120,6 +114,13 @@ export const useBotEffectInternal = () => {
}, 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) {
@@ -130,7 +131,7 @@ export const useBotEffectInternal = () => {
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();
}
@@ -173,36 +174,6 @@ export const useBotEffectInternal = () => {
});
}, [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]);
-
- // resets unread count on opening chat
- useEffect(() => {
- if (isChatWindowOpen) {
- setUnreadCount(0);
- }
- }, [isChatWindowOpen]);
-
// handles scrolling/resizing window on mobile devices
useEffect(() => {
if (isDesktop) {
@@ -293,6 +264,4 @@ export const useBotEffectInternal = () => {
goToPath("start");
}
}, [hasFlowStarted, settings.general?.flowStartTrigger]);
-
- return { timeoutId }
};
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 };
};
diff --git a/src/hooks/internal/useChatHistoryInternal.ts b/src/hooks/internal/useChatHistoryInternal.ts
index da3022b9..bdebf225 100644
--- a/src/hooks/internal/useChatHistoryInternal.ts
+++ b/src/hooks/internal/useChatHistoryInternal.ts
@@ -1,7 +1,9 @@
import { useCallback } from "react";
-import { useRcbEventInternal } from "./useRcbEventInternal";
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,7 +57,9 @@ export const useChatHistoryInternal = () => {
}
}
setIsLoadingChatHistory(true);
- loadChatHistory(settings, styles, chatHistory, setMessages);
+ loadChatHistory(settings, styles, chatHistory, setMessages,
+ chatBodyRef, chatScrollHeight, setIsLoadingChatHistory
+ );
}, [settings, styles, setMessages]);
return { isLoadingChatHistory, setIsLoadingChatHistory, showChatHistory };
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;
+ });
}, []);
/**
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/useIsDesktopInternal.ts b/src/hooks/internal/useIsDesktopInternal.ts
index 9d1279df..cd1fe2ac 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)
diff --git a/src/hooks/internal/useMessagesInternal.ts b/src/hooks/internal/useMessagesInternal.ts
index e9bd5357..986c9da4 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,22 @@ export const useMessagesInternal = () => {
const { messages, setMessages } = useMessagesContext();
// handles bot states
- const { audioToggledOn, isChatWindowOpen, setIsBotTyping, setUnreadCount } = useBotStatesContext();
+ const {
+ audioToggledOn,
+ isChatWindowOpen,
+ isScrolling,
+ 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 +54,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 +94,7 @@ export const useMessagesInternal = () => {
break;
}
}
+ handlePostMessagesUpdate(updatedMessages);
return updatedMessages;
});
}, streamSpeed);
@@ -129,7 +145,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 +180,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 +209,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 +240,7 @@ export const useMessagesInternal = () => {
break;
}
}
-
+ handlePostMessagesUpdate(updatedMessages)
return updatedMessages;
});
return streamMessageMap.current.get(sender) ?? null;
@@ -255,12 +283,64 @@ 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) => {
+ handlePostMessagesUpdate(newMessages);
+ setMessages(newMessages);
+ }
+
+ /**
+ * 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) {
+ shouldNotify = false;
+ }
+
+ // if chatbot is embedded and visible, no need to notify
+ if (settings.general?.embedded && isChatBotVisible(chatBodyRef.current as HTMLDivElement)) {
+ 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") {
+ shouldNotify = false;
+ }
+
+ if (shouldNotify) {
+ playNotificationSound();
+ }
+
+ // 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
+ setTimeout(() => {
+ if (!chatBodyRef.current) {
+ return;
+ }
+
+ chatBodyRef.current.scrollTop = chatBodyRef.current.scrollHeight;
+ }, 1)
+ }
+ }
+
return {
endStreamMessage,
injectMessage,
removeMessage,
streamMessage,
messages,
- setMessages
+ replaceMessages
};
};
diff --git a/src/hooks/internal/usePathsInternal.ts b/src/hooks/internal/usePathsInternal.ts
index c2291a0e..6f34efc7 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..d5a74582 100644
--- a/src/hooks/internal/useSettingsInternal.ts
+++ b/src/hooks/internal/useSettingsInternal.ts
@@ -9,18 +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);
+ }
return {
settings,
- setSettings,
- updateSettings
+ replaceSettings,
+ updateSettings
};
};
diff --git a/src/hooks/internal/useStylesInternal.ts b/src/hooks/internal/useStylesInternal.ts
index 88a0d45f..78d5a0c0 100644
--- a/src/hooks/internal/useStylesInternal.ts
+++ b/src/hooks/internal/useStylesInternal.ts
@@ -9,18 +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);
+ }
return {
styles,
- setStyles,
- updateStyles
+ replaceStyles,
+ updateStyles
};
};
diff --git a/src/hooks/internal/useToastsInternal.ts b/src/hooks/internal/useToastsInternal.ts
index cb6aa466..49819ca2 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
};
};
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);
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