65 lines
2.0 KiB
TypeScript
65 lines
2.0 KiB
TypeScript
|
|
import React, { useEffect, useRef, useState } from 'react';
|
||
|
|
import { useAtomValue } from 'jotai';
|
||
|
|
import { Box, Text, config } from 'folds';
|
||
|
|
|
||
|
|
import { roomIdToMsgDraftAtomFamily } from '../../state/room/roomInputDrafts';
|
||
|
|
import { toPlainText } from '../../components/editor';
|
||
|
|
import { DraftDot, DraftDotPulse, DraftIndicatorBase } from './DraftIndicator.css';
|
||
|
|
|
||
|
|
const PULSE_DURATION = 600;
|
||
|
|
|
||
|
|
type DraftIndicatorProps = {
|
||
|
|
roomId: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Subtle, non-distracting status shown near the composer when the current room
|
||
|
|
* has a persisted (unsent) message draft. It reacts to the shared draft atom
|
||
|
|
* (`roomIdToMsgDraftAtomFamily`) — the same source that backs the
|
||
|
|
* `draft-msg-${roomId}` localStorage persistence — so it never introduces a
|
||
|
|
* parallel persistence path.
|
||
|
|
*
|
||
|
|
* A short "Saved" pulse plays the moment a draft becomes persisted, then the
|
||
|
|
* indicator settles into a quiet, muted resting state. The pulse is gated behind
|
||
|
|
* `prefers-reduced-motion` in CSS, so motion-averse users only ever see the
|
||
|
|
* static label.
|
||
|
|
*/
|
||
|
|
export function DraftIndicator({ roomId }: DraftIndicatorProps) {
|
||
|
|
const draft = useAtomValue(roomIdToMsgDraftAtomFamily(roomId));
|
||
|
|
// Real content, not just an empty paragraph.
|
||
|
|
const hasDraft = toPlainText(draft, false).trim().length > 0;
|
||
|
|
|
||
|
|
const [pulse, setPulse] = useState(false);
|
||
|
|
const hadDraft = useRef(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (hasDraft && !hadDraft.current) {
|
||
|
|
hadDraft.current = true;
|
||
|
|
setPulse(true);
|
||
|
|
const timeout = setTimeout(() => setPulse(false), PULSE_DURATION);
|
||
|
|
return () => clearTimeout(timeout);
|
||
|
|
}
|
||
|
|
hadDraft.current = hasDraft;
|
||
|
|
return undefined;
|
||
|
|
}, [hasDraft]);
|
||
|
|
|
||
|
|
if (!hasDraft) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Box
|
||
|
|
className={DraftIndicatorBase}
|
||
|
|
as="span"
|
||
|
|
shrink="No"
|
||
|
|
alignItems="Center"
|
||
|
|
gap="200"
|
||
|
|
style={{ padding: `0 ${config.space.S100}` }}
|
||
|
|
aria-hidden
|
||
|
|
>
|
||
|
|
<span className={`${DraftDot}${pulse ? ` ${DraftDotPulse}` : ''}`} />
|
||
|
|
<Text as="span" size="T200" priority="300">
|
||
|
|
Draft saved
|
||
|
|
</Text>
|
||
|
|
</Box>
|
||
|
|
);
|
||
|
|
}
|