Compare commits

..
Author SHA1 Message Date
Lotus CIandClaude Opus 5.5 36e697613b fix(messages): the delivery indicator updates when a send completes (#306)
CI / Build & Quality Checks (pull_request) Successful in 3m47s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 13s
CI / Playwright smoke (e2e) (pull_request) Successful in 14m53s
DeliveryStatus showed whatever mEvent.status was at the last timeline
re-render, and a local echo's status changes (SENDING → SENT → confirmed,
NOT_SENT → retried) only emit event-level events, so the last message in
a quiet room kept "Sending..." (or a stale "Failed to send. Click to
retry" after an outbox retry) indefinitely. It now re-renders on
MatrixEventEvent.Status / LocalEventIdReplaced.

Not browser-verified in this change (checks: tsc, unit tests, eslint).

Fixes #306

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-16 22:25:41 -04:00
2 changed files with 19 additions and 15 deletions
+16 -1
View File
@@ -31,12 +31,13 @@ import React, {
MouseEventHandler,
ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import FocusTrap from 'focus-trap-react';
import { useHover, useFocusWithin } from 'react-aria';
import { MatrixEvent, Room, EventStatus } from 'matrix-js-sdk';
import { MatrixEvent, MatrixEventEvent, Room, EventStatus } from 'matrix-js-sdk';
import { Relations } from 'matrix-js-sdk/lib/models/relations';
import classNames from 'classnames';
import { useAtom, useAtomValue } from 'jotai';
@@ -114,6 +115,20 @@ function DeliveryStatus({
}) {
const mx = useMatrixClient();
const offline = useAtomValue(sendOfflineAtom);
// [Gitea #306] A local echo's status changes (SENDING → SENT → confirmed,
// NOT_SENT → retried) don't re-render the timeline, so the last message in a
// quiet room kept "Sending..." (or a stale "Failed to send") indefinitely.
// Re-render on the event's own status updates.
const [, setStatusTick] = useState(0);
useEffect(() => {
const bump = () => setStatusTick((t) => t + 1);
mEvent.on(MatrixEventEvent.Status, bump);
mEvent.on(MatrixEventEvent.LocalEventIdReplaced, bump);
return () => {
mEvent.off(MatrixEventEvent.Status, bump);
mEvent.off(MatrixEventEvent.LocalEventIdReplaced, bump);
};
}, [mEvent]);
const { status } = mEvent;
if (status === null) return null; // confirmed by server — read receipts take over
let iconSrc: IconSrc;
+3 -14
View File
@@ -20,12 +20,9 @@ export const IDB_VERSION_CONFLICT = 'IDB_VERSION_CONFLICT';
* session. When that happens the device "resurrects" with a blank key store and the
* client re-uploads a one-time key at an id Synapse already holds → a permanent
* `400 M_UNKNOWN: … already exists` upload-conflict storm (and, downstream,
* undecryptable to-device/media keys). Chromium/WebKit answer `persist()` at once
* (engagement / PWA install); Firefox shows a permission prompt and the promise
* stays pending until it's answered. Denial/absence is non-fatal.
* undecryptable to-device/media keys). `persist()` grants based on engagement / PWA
* install and shows no prompt; denial/absence is non-fatal.
*/
const PERSIST_WAIT_MS = 1500;
export const requestPersistentStorage = async (): Promise<boolean> => {
try {
if (!navigator.storage?.persist) return false;
@@ -42,15 +39,7 @@ export const requestPersistentStorage = async (): Promise<boolean> => {
export const initClient = async (session: Session): Promise<MatrixClient> => {
// Protect the crypto store from eviction before we create and write to it.
// [Gitea #303] Never block startup on the answer: Firefox prompts and leaves
// the promise pending until the user responds, which froze the app on
// "Heating up". KE-1 only needs the request made; give it 1.5 s at most.
await Promise.race([
requestPersistentStorage(),
new Promise<boolean>((resolve) => {
setTimeout(() => resolve(false), PERSIST_WAIT_MS);
}),
]);
await requestPersistentStorage();
const indexedDBStore = new IndexedDBStore({
indexedDB: globalThis.indexedDB,