feat: offline outbox — unsent messages survive reload and retry (#112)
CI / Build & Quality Checks (pull_request) Successful in 6m4s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 27s
CI / Docker image build & smoke test (pull_request) Skipped
CI / Playwright smoke (e2e) (pull_request) Successful in 10m28s
CI / Build & Quality Checks (pull_request) Successful in 6m4s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 27s
CI / Docker image build & smoke test (pull_request) Skipped
CI / Playwright smoke (e2e) (pull_request) Successful in 10m28s
Until now a send that failed (offline, homeserver down, a blip) went straight to "Failed to send": nothing retried it, and a reload dropped it without trace (chronological pending ordering keeps local echoes in memory only). - Outbox (utils/outbox.ts + features/outbox/OutboxFeature): own message sends (text, stickers, reactions, polls; not call signalling or redactions) are mirrored to localStorage from their first local echo until the server confirms them or the user cancels. - After a reload they come back as local echoes via room.addPendingEvent, same shape as the SDK's own. Recent ones (< 1 h) are sent again with the same txnId; older ones come back as "Failed to send" for the user to retry or cancel. Ones the server already has (transaction id seen in /sync) are dropped, so no duplicates. - Retries: network failures (ConnectionError, 408/429/5xx) are re-sent when the connection returns (sync recovers or the browser goes back online), and after a blip while online (5 s, backing off, max 10 per message). Oldest first, in order per room. 4xx / consent / encryption failures are left to the user. - UI: a network failure while offline shows a clock, "Queued. Will send when you're back online" (thread view too), not the red ✕. The ✕ is now a button: click to retry. - Logout wipes the outbox with the other plaintext caches (the content is decrypted, like drafts). Tested end to end against a local Synapse (Chromium): offline → queued → sent once on reconnect; homeserver unreachable → queued → sent once; failed send → reload → sent once and shown once; server accepted but response lost → reload → no duplicate; 2 h old entry → failed, not sent, click ✕ → sent; cancel → gone after reload; encrypted room → restored message goes out as m.room.encrypted with no plaintext and decrypts; one-off failure retried by itself in ~5 s; no page errors. Unit tests for the pure parts; Playwright 20 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c0c93213c1
commit
02d86caeb0
@@ -36,6 +36,7 @@ test('clearPlaintextCaches removes every plaintext/PII localStorage key', () =>
|
||||
store.set('cinny_recent_forward_targets_v1', '[]');
|
||||
store.set('cinny_recent_gifs_v1', '[]');
|
||||
store.set('cinny_recent_stickers_v1', '[]');
|
||||
store.set('lotus_outbox_v1', '{"userId":"@me:server","entries":[]}');
|
||||
removed.length = 0;
|
||||
|
||||
clearPlaintextCaches();
|
||||
@@ -47,6 +48,7 @@ test('clearPlaintextCaches removes every plaintext/PII localStorage key', () =>
|
||||
'cinny_recent_forward_targets_v1',
|
||||
'cinny_recent_gifs_v1',
|
||||
'cinny_recent_stickers_v1',
|
||||
'lotus_outbox_v1', // [Gitea #112] unsent message content
|
||||
]) {
|
||||
assert.ok(removed.includes(key), `${key} cleared`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user