diff --git a/frontend-new/src/chat/Chat.test.tsx b/frontend-new/src/chat/Chat.test.tsx
index 75995e709..298902e6e 100644
--- a/frontend-new/src/chat/Chat.test.tsx
+++ b/frontend-new/src/chat/Chat.test.tsx
@@ -2251,6 +2251,58 @@ describe("Chat", () => {
expect(console.warn).not.toHaveBeenCalled();
});
+ test("should not re-render chat list on keydown when backdrop is already hidden", async () => {
+ // GIVEN a logged-in user with an active session
+ const givenUser: TabiyaUser = getMockUser();
+ AuthenticationStateService.getInstance().setUser(givenUser);
+ const givenActiveSessionId = 123;
+ UserPreferencesStateService.getInstance().setUserPreferences(
+ getMockUserPreferences(givenUser, givenActiveSessionId)
+ );
+
+ // AND a chat history with a message
+ const givenMessages: ConversationResponse = getMockConversationResponse(
+ [
+ {
+ message_id: nanoid(),
+ message: "hello",
+ sent_at: new Date().toISOString(),
+ sender: ConversationMessageSender.USER,
+ reaction: null,
+ },
+ ],
+ ConversationPhase.DIVE_IN,
+ 75
+ );
+ jest.spyOn(ChatService.getInstance(), "getChatHistory").mockResolvedValueOnce(givenMessages);
+
+ // WHEN the component is mounted
+ render();
+
+ // AND the chat component is initialized
+ await assertChatInitialized();
+
+ // Capture the current render count after initialization settles
+ const initialRenderCount = (ChatList as jest.Mock).mock.calls.length;
+
+ // WHEN multiple keydown events are fired (user typing)
+ await act(async () => {
+ for (let i = 0; i < 5; i++) {
+ document.dispatchEvent(
+ new KeyboardEvent("keydown", {
+ key: "a",
+ code: "KeyA",
+ bubbles: true,
+ cancelable: true,
+ })
+ );
+ }
+ });
+
+ // THEN no additional ChatList renders should occur (no re-render per keystroke)
+ expect((ChatList as jest.Mock).mock.calls.length).toBe(initialRenderCount);
+ });
+
test.each([
[
"click",
diff --git a/frontend-new/src/chat/Chat.tsx b/frontend-new/src/chat/Chat.tsx
index 49d00bc33..fdf4ea424 100644
--- a/frontend-new/src/chat/Chat.tsx
+++ b/frontend-new/src/chat/Chat.tsx
@@ -117,7 +117,6 @@ export const Chat: React.FC> = ({
const [isLoading, setIsLoading] = React.useState(false);
const [isLoggingOut, setIsLoggingOut] = React.useState(false);
const [showBackdrop, setShowBackdrop] = useState(showInactiveSessionAlert);
- const [lastActivityTime, setLastActivityTime] = React.useState(Date.now());
const [newConversationDialog, setNewConversationDialog] = React.useState(false);
const [exploredExperiencesNotification, setExploredExperiencesNotification] = useState(false);
const [activeSessionId, setActiveSessionId] = useState(
@@ -134,6 +133,7 @@ export const Chat: React.FC> = ({
timeoutId: NodeJS.Timeout
}>>(new Map());
+ const lastActivityRef = useRef(Date.now());
const navigate = useNavigate();
const initializingRef = useRef(false);
@@ -782,7 +782,7 @@ export const Chat: React.FC> = ({
if (disableInactivityCheck || conversationCompleted) return;
const checkInactivity = () => {
- if (Date.now() - lastActivityTime > INACTIVITY_TIMEOUT) {
+ if (Date.now() - lastActivityRef.current > INACTIVITY_TIMEOUT) {
setShowBackdrop(true);
}
};
@@ -790,22 +790,24 @@ export const Chat: React.FC> = ({
const interval = setInterval(checkInactivity, CHECK_INACTIVITY_INTERVAL);
return () => clearInterval(interval);
- }, [lastActivityTime, disableInactivityCheck, conversationCompleted]);
+ }, [disableInactivityCheck, conversationCompleted]);
- // Close backdrop when user interacts with the page
+ // Close the backdrop when the user interacts with the page
useEffect(() => {
if (disableInactivityCheck) return;
// Reset the timer when the user interacts with the page
const resetTimer = () => {
- setLastActivityTime(Date.now());
- setShowBackdrop(false);
+ lastActivityRef.current = Date.now();
+ setShowBackdrop((prev) => (prev ? false : prev));
};
const events = ["mousedown", "keydown"];
- events.forEach((event) => document.addEventListener(event, resetTimer));
+ events.forEach((event) => document.addEventListener(event, resetTimer, { passive: true }));
- return () => events.forEach((event) => document.removeEventListener(event, resetTimer));
+ return () => {
+ events.forEach((e) => document.removeEventListener(e, resetTimer));
+ };
}, [disableInactivityCheck]);
// Preload the "Download Report" button when experiences are explored.