Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions frontend-new/src/chat/Chat.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Chat />);

// 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",
Expand Down
18 changes: 10 additions & 8 deletions frontend-new/src/chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ export const Chat: React.FC<Readonly<ChatProps>> = ({
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [isLoggingOut, setIsLoggingOut] = React.useState<boolean>(false);
const [showBackdrop, setShowBackdrop] = useState(showInactiveSessionAlert);
const [lastActivityTime, setLastActivityTime] = React.useState<number>(Date.now());
const [newConversationDialog, setNewConversationDialog] = React.useState<boolean>(false);
const [exploredExperiencesNotification, setExploredExperiencesNotification] = useState<boolean>(false);
const [activeSessionId, setActiveSessionId] = useState<number | null>(
Expand All @@ -134,6 +133,7 @@ export const Chat: React.FC<Readonly<ChatProps>> = ({
timeoutId: NodeJS.Timeout
}>>(new Map());

const lastActivityRef = useRef(Date.now());
const navigate = useNavigate();

const initializingRef = useRef(false);
Expand Down Expand Up @@ -782,30 +782,32 @@ export const Chat: React.FC<Readonly<ChatProps>> = ({
if (disableInactivityCheck || conversationCompleted) return;

const checkInactivity = () => {
if (Date.now() - lastActivityTime > INACTIVITY_TIMEOUT) {
if (Date.now() - lastActivityRef.current > INACTIVITY_TIMEOUT) {
setShowBackdrop(true);
}
};
// Check for inactivity
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.
Expand Down
Loading