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
jared 400ee76b63 Merge pull request 'fix(calls): no "Missed call" in voice rooms or space channels' (#363) from fix/missed-call-voice-rooms into lotus
CI / Build & Quality Checks (push) Successful in 1m44s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 13s
CI / Trigger Desktop Build (push) Successful in 3s
CI / Playwright smoke (e2e) (push) Successful in 10m58s
2026-10-16 21:06:57 -04:00
jared 47c1e435b7 Merge pull request 'perf(backgrounds): animated chat backgrounds cost ~5% CPU instead of 50 to 70% (#337)' (#361) from fix/animated-bg-compositor into lotus
CI / Build & Quality Checks (push) Successful in 1m42s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 11s
CI / Trigger Desktop Build (push) Canceled after 8s
CI / Playwright smoke (e2e) (push) Canceled after 0s
2026-10-16 20:52:34 -04:00
jared d55a8ba8a5 Merge branch 'lotus' into fix/animated-bg-compositor
CI / Build & Quality Checks (pull_request) Successful in 1m52s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 9s
CI / Playwright smoke (e2e) (pull_request) Successful in 11m11s
2026-10-16 19:59:31 -04:00
Lotus CIandClaude Opus 5.5 2700aad209 perf(backgrounds): animated chat backgrounds cost ~5% CPU instead of 50 to 70% (#337)
CI / Build & Quality Checks (pull_request) Successful in 1m57s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 9s
CI / Playwright smoke (e2e) (pull_request) Failing after 9m56s
background-position can't run on the compositor, so every frame of the
animated chat backgrounds repainted the whole chat area: 51 to 72% main-thread
busy while idle in a room at 60 fps. The drifts are slow (3 to 22 px/s,
under a pixel per frame), so each animation now uses steps(): about 12
updates/s for fireflies, stars and aurora and about 24/s for rain and
pulse.

Measured headless (20 s idle in a room, CDP TaskDuration):
  none 0.7% → 0.9%; rain 50.7 → 5.9; stars 71.2 → 4.5;
  pulse 64.9 → 5.8; aurora 58.9 → 4.2; fireflies 71.7 → 3.8.

Aurora and pulse used ease-in-out; with steps() their motion is now
even-paced. Pause animations / reduced motion still strip the animation.

Fixes #337

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-16 19:05:52 -04:00
7 changed files with 31 additions and 11 deletions
@@ -51,7 +51,7 @@ const dark: CSSProperties = {
backgroundSize: '260% 240%, 300% 260%, 240% 280%, 220% 200%, 100% 100%, 100% 100%',
backgroundPosition: '0% 30%, 100% 70%, 50% 0%, 20% 80%, 50% 50%, 50% 50%',
backgroundRepeat: 'no-repeat',
animation: `${auroraFlow} 60s ease-in-out infinite`,
animation: `${auroraFlow} 60s steps(180) infinite`,
};
const light: CSSProperties = {
@@ -75,7 +75,7 @@ const light: CSSProperties = {
backgroundSize: '260% 240%, 300% 260%, 240% 280%, 220% 200%, 100% 100%, 100% 100%',
backgroundPosition: '0% 30%, 100% 70%, 50% 0%, 20% 80%, 50% 50%, 50% 50%',
backgroundRepeat: 'no-repeat',
animation: `${auroraFlow} 60s ease-in-out infinite`,
animation: `${auroraFlow} 60s steps(180) infinite`,
};
export const animAurora: ChatBgVariants = { dark, light };
@@ -55,7 +55,7 @@ export const animFireflies: ChatBgVariants = {
'0 0', // wash A (static)
'0 0', // wash B (static)
].join(','),
animation: `${firefliesDrift} 44s linear infinite`,
animation: `${firefliesDrift} 44s steps(528) infinite`,
},
// Light: a cozy warm dim-dusk. No harsh dots on white — soft amber motes with
@@ -93,6 +93,6 @@ export const animFireflies: ChatBgVariants = {
'0 0', // wash A (static)
'0 0', // wash B (static)
].join(','),
animation: `${firefliesDrift} 44s linear infinite`,
animation: `${firefliesDrift} 44s steps(528) infinite`,
},
};
@@ -76,7 +76,7 @@ export const animPulse: ChatBgVariants = {
'0 0', // base wash
'0 0', // vignette
].join(','),
animation: `${gridPulse} 22s ease-in-out infinite`,
animation: `${gridPulse} 22s steps(528) infinite`,
},
// Light: soft slate-blue grid on pale cool-white, a gentle luminance breathe.
@@ -116,6 +116,6 @@ export const animPulse: ChatBgVariants = {
'0 0', // base wash
'0 0', // vignette
].join(','),
animation: `${gridPulse} 22s ease-in-out infinite`,
animation: `${gridPulse} 22s steps(528) infinite`,
},
};
@@ -97,7 +97,7 @@ export const animRain: ChatBgVariants = {
].join(','),
backgroundSize: ['260px 200px', '100% 100%', '100% 100%', '100% 100%'].join(','),
backgroundPosition: ['0 0', '0 0', '0 0', '0 0'].join(','),
animation: `${rainFall} 12s linear infinite`,
animation: `${rainFall} 12s steps(288) infinite`,
},
// Light: soft teal-grey streaks on a pale cool base — elegant, never neon.
@@ -118,6 +118,6 @@ export const animRain: ChatBgVariants = {
].join(','),
backgroundSize: ['260px 200px', '100% 100%', '100% 100%', '100% 100%'].join(','),
backgroundPosition: ['0 0', '0 0', '0 0', '0 0'].join(','),
animation: `${rainFall} 12s linear infinite`,
animation: `${rainFall} 12s steps(288) infinite`,
},
};
@@ -68,7 +68,7 @@ export const animStars: ChatBgVariants = {
'0 0', // nebula A
'0 0', // nebula B
].join(','),
animation: `${starDrift} 90s linear infinite`,
animation: `${starDrift} 90s steps(1080) infinite`,
},
// Light: an airy pre-dawn sky. No literal white-on-white stars — instead very
@@ -112,6 +112,6 @@ export const animStars: ChatBgVariants = {
'0 0', // wash A
'0 0', // wash B
].join(','),
animation: `${starDrift} 100s linear infinite`,
animation: `${starDrift} 100s steps(1200) infinite`,
},
};
+5
View File
@@ -149,6 +149,11 @@ export const getChatBg = (
}
// For animated backgrounds, promote the element to its own compositor layer so
// background-position keyframes don't trigger repaints on descendant elements.
// [Gitea #337] background-position can't run on the compositor, so every
// animation frame repaints the whole chat area (45 to 70% main-thread busy
// at 60 fps). The drifts are slow (3–22 px/s), so each animation uses steps():
// ~12 updates/s for the slow ones, ~24/s for rain and pulse: visually the
// same, a fraction of the repaints.
if (style.animation) {
return { ...style, willChange: 'background-position', contain: 'paint' };
}
+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;