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:
@@ -1,6 +1,6 @@
|
|||||||
import { Popover } from 'antd';
|
import { Popover } from 'antd';
|
||||||
import React, { FC, useEffect, useState } from 'react';
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
import Graphemer from 'graphemer';
|
import Graphemer from 'graphemer';
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import dynamic from 'next/dynamic';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import ContentEditable from './ContentEditable';
|
import ContentEditable from './ContentEditable';
|
||||||
import WebsocketService from '../../../services/websocket-service';
|
import WebsocketService from '../../../services/websocket-service';
|
||||||
import { websocketServiceAtom } from '../../stores/ClientConfigStore';
|
import { websocketServiceAtom, chatInputDraftAtom } from '../../stores/ClientConfigStore';
|
||||||
import { MessageType } from '../../../interfaces/socket-events';
|
import { MessageType } from '../../../interfaces/socket-events';
|
||||||
import styles from './ChatTextField.module.scss';
|
import styles from './ChatTextField.module.scss';
|
||||||
|
|
||||||
@@ -120,6 +120,7 @@ const getTextContent = node => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, focusInput }) => {
|
export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, focusInput }) => {
|
||||||
|
const [inputDraft, setInputDraft] = useRecoilState(chatInputDraftAtom);
|
||||||
const [characterCount, setCharacterCount] = useState(defaultText?.length);
|
const [characterCount, setCharacterCount] = useState(defaultText?.length);
|
||||||
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
||||||
const [contentEditable, setContentEditable] = useState(null);
|
const [contentEditable, setContentEditable] = useState(null);
|
||||||
@@ -146,6 +147,7 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
|||||||
|
|
||||||
websocketService.send({ type: MessageType.CHAT, body: message });
|
websocketService.send({ type: MessageType.CHAT, body: message });
|
||||||
contentEditable.innerHTML = '';
|
contentEditable.innerHTML = '';
|
||||||
|
setInputDraft('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const insertTextAtEnd = (textToInsert: string) => {
|
const insertTextAtEnd = (textToInsert: string) => {
|
||||||
@@ -220,6 +222,9 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
|||||||
contentEditable.removeChild(contentEditable.children[0]);
|
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.
|
// 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 });
|
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 () => {
|
const getCustomEmoji = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/emoji`);
|
const response = await fetch(`/api/emoji`);
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ export const chatAuthenticatedAtom = atom<boolean>({
|
|||||||
default: false,
|
default: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Stores chat input draft to preserve text across mobile/desktop mode switches
|
||||||
|
export const chatInputDraftAtom = atom<string>({
|
||||||
|
key: 'chatInputDraftAtom',
|
||||||
|
default: '',
|
||||||
|
});
|
||||||
|
|
||||||
export const websocketServiceAtom = atom<WebsocketService>({
|
export const websocketServiceAtom = atom<WebsocketService>({
|
||||||
key: 'websocketServiceAtom',
|
key: 'websocketServiceAtom',
|
||||||
default: null,
|
default: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user