fix: preserve chat input text across window resize (#4717)

Adds a Recoil atom (chatInputDraftAtom) to store the chat input draft,
which persists the text when the chat component unmounts/remounts during
mobile/desktop mode transitions.

Changes:
- Add chatInputDraftAtom to ClientConfigStore
- Update ChatTextField to save draft on input change
- Restore draft when component mounts
- Clear draft when message is sent

Closes #3615
This commit is contained in:
John Costa
2026-01-17 14:33:02 -08:00
committed by GitHub
parent a82efb0e9a
commit 9f169ce96e
2 changed files with 21 additions and 2 deletions
@@ -1,6 +1,6 @@
import { Popover } from 'antd';
import React, { FC, useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import sanitizeHtml from 'sanitize-html';
import Graphemer from 'graphemer';
@@ -8,7 +8,7 @@ import dynamic from 'next/dynamic';
import classNames from 'classnames';
import ContentEditable from './ContentEditable';
import WebsocketService from '../../../services/websocket-service';
import { websocketServiceAtom } from '../../stores/ClientConfigStore';
import { websocketServiceAtom, chatInputDraftAtom } from '../../stores/ClientConfigStore';
import { MessageType } from '../../../interfaces/socket-events';
import styles from './ChatTextField.module.scss';
@@ -120,6 +120,7 @@ const getTextContent = node => {
};
export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, focusInput }) => {
const [inputDraft, setInputDraft] = useRecoilState(chatInputDraftAtom);
const [characterCount, setCharacterCount] = useState(defaultText?.length);
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
const [contentEditable, setContentEditable] = useState(null);
@@ -146,6 +147,7 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
websocketService.send({ type: MessageType.CHAT, body: message });
contentEditable.innerHTML = '';
setInputDraft('');
};
const insertTextAtEnd = (textToInsert: string) => {
@@ -220,6 +222,9 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
contentEditable.removeChild(contentEditable.children[0]);
}
}
// Persist draft to Recoil state so it survives mobile/desktop mode switches
setInputDraft(contentEditable.innerHTML);
};
// Focus the input when the component mounts.
@@ -230,6 +235,14 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
document.getElementById('chat-input-content-editable').focus({ preventScroll: true });
}, []);
// Restore draft from Recoil state when component mounts (e.g., after mobile/desktop switch)
useEffect(() => {
if (contentEditable && inputDraft) {
contentEditable.innerHTML = inputDraft;
setCharacterCount(graphemer.countGraphemes(getTextContent(contentEditable)));
}
}, [contentEditable]);
const getCustomEmoji = async () => {
try {
const response = await fetch(`/api/emoji`);