49 lines
1.5 KiB
TypeScript
49 lines
1.5 KiB
TypeScript
|
|
import { useEffect } from 'react';
|
||
|
|
import { useTranslation } from 'react-i18next';
|
||
|
|
|
||
|
|
import type { AuthorDirectory } from '@/components/chat/useAuthorDirectory';
|
||
|
|
import { useMessagesStore, type TypingEntry } from '@/stores/messages';
|
||
|
|
|
||
|
|
interface TypingIndicatorProps {
|
||
|
|
channelId: string;
|
||
|
|
typing: TypingEntry[];
|
||
|
|
authors: AuthorDirectory;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Как часто проверяем, не погасли ли события `TYPING_START`. */
|
||
|
|
const PRUNE_INTERVAL_MS = 1000;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Строка «печатает…» под лентой. Живёт 6 секунд с последнего события
|
||
|
|
* `TYPING_START` (событие приходит раз в ~3 с, пока пользователь пишет).
|
||
|
|
*/
|
||
|
|
export function TypingIndicator({ channelId, typing, authors }: TypingIndicatorProps) {
|
||
|
|
const { t } = useTranslation();
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (typing.length === 0) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const timer = window.setInterval(() => {
|
||
|
|
useMessagesStore.getState().pruneTyping();
|
||
|
|
}, PRUNE_INTERVAL_MS);
|
||
|
|
return () => {
|
||
|
|
window.clearInterval(timer);
|
||
|
|
};
|
||
|
|
}, [typing.length, channelId]);
|
||
|
|
|
||
|
|
if (typing.length === 0) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
const names = typing
|
||
|
|
.map((entry) => authors.resolve(entry.user_id).name)
|
||
|
|
.filter((name) => name !== '');
|
||
|
|
|
||
|
|
return (
|
||
|
|
<p role="status" data-testid="typing-indicator" className="px-4 pb-1 text-xs text-fg-muted">
|
||
|
|
{t('chat.typing', { count: names.length, names: names.join(', ') })}
|
||
|
|
</p>
|
||
|
|
);
|
||
|
|
}
|