mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 08:06:13 +00:00
fix: count down the seconds left on the undo-send bar
This commit is contained in:
@@ -8,7 +8,7 @@ import { spacing, radius, typography, type ThemePalette } from '../theme/tokens'
|
||||
import { useColors } from '../theme/colors';
|
||||
import { undoSnackbarColors } from './undo-snackbar-colors';
|
||||
import { useEmailStore } from '../stores/email-store';
|
||||
import { useSendUndoStore, restoreUndoneSend, type PendingUndoSend } from '../stores/send-undo-store';
|
||||
import { useSendUndoStore, restoreUndoneSend, undoSendLabel, type PendingUndoSend } from '../stores/send-undo-store';
|
||||
import { useLocaleStore } from '../stores/locale-store';
|
||||
import type { RootStackParamList } from '../navigation/types';
|
||||
import { announce } from '../lib/announce';
|
||||
@@ -56,7 +56,8 @@ export function UndoSnackbar() {
|
||||
: null;
|
||||
const send: Shown | null = pendingSend
|
||||
? {
|
||||
label: t('email_composer.undo_send_label', 'Sending in {seconds}s…', { seconds: pendingSend.delaySeconds }),
|
||||
// The text at the start; the bar counts down from it (below).
|
||||
label: undoSendLabel(pendingSend, pendingSend.createdAt),
|
||||
createdAt: pendingSend.createdAt,
|
||||
visibleMs: Math.max(1000, pendingSend.delaySeconds * 1000 - 1000),
|
||||
send: pendingSend,
|
||||
@@ -74,6 +75,16 @@ export function UndoSnackbar() {
|
||||
if (current) setShown(current);
|
||||
}, [current]);
|
||||
|
||||
// A held send counts down: "Sending in 10s…", "Sending in 9s…".
|
||||
const [now, setNow] = React.useState(() => Date.now());
|
||||
const counting = current?.send;
|
||||
React.useEffect(() => {
|
||||
if (!counting) return undefined;
|
||||
setNow(Date.now());
|
||||
const id = setInterval(() => setNow(Date.now()), 250);
|
||||
return () => clearInterval(id);
|
||||
}, [counting]);
|
||||
|
||||
// Read the bar out on iOS, which has no live regions; Android reads the
|
||||
// bar's live region. Once per entry, not on every countdown re-render.
|
||||
const undoLabel = t('common.undo', 'Undo');
|
||||
@@ -148,7 +159,11 @@ export function UndoSnackbar() {
|
||||
]}
|
||||
>
|
||||
<View style={styles.bar} accessibilityLiveRegion="polite">
|
||||
<Text style={styles.label} numberOfLines={1}>{shown.label}</Text>
|
||||
{/* The spoken label stays the first one, so the live region is not
|
||||
read out again every second of the countdown. */}
|
||||
<Text style={styles.label} numberOfLines={1} accessibilityLabel={shown.label}>
|
||||
{shown.send ? undoSendLabel(shown.send, now) : shown.label}
|
||||
</Text>
|
||||
{shown.send && (
|
||||
<Pressable
|
||||
onPress={onSendNow}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
vi.mock('../../api/jmap-client', () => ({
|
||||
jmapClient: { accountId: 'primary', connectedAccountId: 'primary', isConnected: true },
|
||||
RequestTimeoutError: class RequestTimeoutError extends Error {},
|
||||
}));
|
||||
|
||||
import { undoSendLabel, undoSendSecondsLeft } from '../../stores/send-undo-store';
|
||||
|
||||
// The undo-send bar said "Sending in 10s…" for the whole delay: its text was
|
||||
// built once from the delay. It counts down now. No React Native renderer in
|
||||
// the test env, so the bar's wiring is checked in its source.
|
||||
|
||||
const read = (rel: string) => fs.readFileSync(path.resolve(__dirname, '..', '..', rel), 'utf8');
|
||||
const HELD = { createdAt: 1_000_000, delaySeconds: 10 };
|
||||
|
||||
describe('undo-send countdown', () => {
|
||||
it('counts the seconds left down from the delay', () => {
|
||||
expect(undoSendSecondsLeft(HELD, HELD.createdAt)).toBe(10);
|
||||
expect(undoSendSecondsLeft(HELD, HELD.createdAt + 400)).toBe(10);
|
||||
expect(undoSendSecondsLeft(HELD, HELD.createdAt + 3_500)).toBe(7);
|
||||
expect(undoSendSecondsLeft(HELD, HELD.createdAt + 9_000)).toBe(1);
|
||||
// Never 0 or less, never more than the delay (a clock set back).
|
||||
expect(undoSendSecondsLeft(HELD, HELD.createdAt + 20_000)).toBe(1);
|
||||
expect(undoSendSecondsLeft(HELD, HELD.createdAt - 5_000)).toBe(10);
|
||||
});
|
||||
|
||||
it('builds the bar text for the moment it is drawn', () => {
|
||||
expect(undoSendLabel(HELD, HELD.createdAt)).toBe('Sending in 10s…');
|
||||
expect(undoSendLabel(HELD, HELD.createdAt + 4_200)).toBe('Sending in 6s…');
|
||||
});
|
||||
|
||||
it('redraws the bar text from a ticking clock', () => {
|
||||
const src = read('components/UndoSnackbar.tsx');
|
||||
expect(src).toMatch(/setInterval\(\(\) => setNow\(Date\.now\(\)\), \d+\)/);
|
||||
expect(src).toMatch(/\{shown\.send \? undoSendLabel\(shown\.send, now\) : shown\.label\}/);
|
||||
});
|
||||
});
|
||||
@@ -77,6 +77,22 @@ function refuseOtherLogin(entry: PendingUndoSend): void {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whole seconds until a held send goes out, counted on this device's clock
|
||||
* from when the delay started: the bar counts down "Sending in 10s…",
|
||||
* "Sending in 9s…" and so on. Never below 1, since the bar closes a second
|
||||
* before the release.
|
||||
*/
|
||||
export function undoSendSecondsLeft(entry: Pick<PendingUndoSend, 'createdAt' | 'delaySeconds'>, now: number): number {
|
||||
const left = Math.ceil((entry.createdAt + entry.delaySeconds * 1000 - now) / 1000);
|
||||
return Math.max(1, Math.min(entry.delaySeconds, left));
|
||||
}
|
||||
|
||||
/** The undo bar's text for a held send at `now`. */
|
||||
export function undoSendLabel(entry: Pick<PendingUndoSend, 'createdAt' | 'delaySeconds'>, now: number): string {
|
||||
return t('email_composer.undo_send_label', 'Sending in {seconds}s…', { seconds: undoSendSecondsLeft(entry, now) });
|
||||
}
|
||||
|
||||
interface SendUndoState {
|
||||
pending: PendingUndoSend | null;
|
||||
busy: boolean;
|
||||
|
||||
Reference in New Issue
Block a user