Status messages are saved per device and re-sent with every presence
heartbeat (a presence write without status_msg clears it on Synapse). A
device never receives its own user's presence changes made on other
devices, so the DP3 fix in db864326 — mirroring remote changes from the
Profile page — could never fire: device B kept a status that device A had
cleared and re-published it on its next state change.
Heartbeats now reconcile with the server first: GET our own presence,
send the server's current status_msg and bring the local copy in line.
Falls back to the local copy when the read fails, when the server shows
us offline (invisible mode clears the status by design), and for 15 s
after this device saved/cleared its own status (a server read that
hasn't caught up yet can't override a fresh save).
Verified with two sessions of the same user against local Synapse:
B sets "dp3 old status" → A clears it → B goes hidden→visible → server
stays "" and B's local copy is removed (before: back to "dp3 old status").
A sets "dp3 new from A" → B heartbeat keeps it and adopts it locally.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
Reported: 'Failed to save status — server may be rate limiting. Try again.'
on the first status change in a month. Root cause: Synapse rate-limits
PUT /presence/{user}/status to ONE request per 10 s per user by default
(rc_presence.per_user: per_second 0.1, burst_count 1), shared across all of
the user's devices, and our presence heartbeat (online/away on visibility and
activity changes, from every open tab/device) spends that budget — so a manual
save that lands within 10 s of a heartbeat gets a 429, which the form showed
as a dead end.
Two fixes: (1) the status save waits out Retry-After (bounded to ~25 s) via
setPresenceWithRetry instead of failing, and the error text now says what
actually happened (rate limit / server text / offline); (2) the heartbeat
dedupes — it only sends when presence or status actually changes (/sync
already keeps us online), so it stops burning the budget in the first place.
Unit-tested; reproduced headless with a routed 10 s limiter: heartbeat ok →
save 429 → retried 8 s later → saved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
Wave-3 bug-hunt fixes (findings in LOTUS_TODO), reviewed + gate-green:
- 🔴 ACL editor [H1–H4]: block saving an empty allow-list (was a one-click
federation brick), warn on self-ban (case-insensitive glob match of
mx.getDomain() vs allow/deny), accept real globs (1.2.3.*, *.evil.*), and
gate Save behind a confirm dialog.
- 🔴 [P1] room context menu no longer acts on the wrong room after a live
reorder (key by roomId, not list index). 🔴 [P2] status writes no longer
force presence to online over Invisible/DND (shared presenceStateFromSetting).
- 🟠 [P3] timed mutes restored on boot; [P4] custom-status auto-clear now fires
(always-mounted StatusExpiryMonitor); [P5] timezone also PUT to the m.tz
profile field so it's visible to others; [H6] RoomInsights single-pass
min/max (was Math.min(...spread) stack overflow); [H7/H8] mod-log labels.
- 🟡 [P6/P7] favorites collapse+filter, [P8] charCount reset, [P9] DM preview
refresh on decrypt; theming [T-P1] lazy decorations, [T-P2] drop the redundant
always-on body animation, [T-P4] live useReducedMotion, [T-P5] decoration key.
- NATIVE-CINNY LAW: notification presets + Powers permissions use folds icons.
DEFERRED: [H5] invite-QR is fetched from api.qrserver.com (third-party leak);
local generation needs a bundled QR lib (not added). tsc/eslint/prettier clean,
build OK, 677 tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- uploadContent: bounded retry (max 3) reusing rateLimitedActions' capped
exponential backoff; retries only transient failures (network/429/5xx), never 4xx.
- Robust MatrixError construction from UploadResponse / unknown error shapes.
- addRoomIdToMDirect/removeRoomIdFromMDirect: drop `as any`, use typed
EventType.Direct + MDirectContent.
- usePresenceUpdater: keep fetch({keepalive}) for the unload offline-presence
update (sendBeacon can't set the auth header) and log redacted warnings instead
of silently swallowing presence errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ML noise suppression produced loud static on real calls. RNNoise requires
mono 48kHz float input; feeding it stereo or wrong-rate data is the classic
cause of that static. Harden the shim:
- request mono (channelCount:1) + 48kHz capture
- run a 48kHz AudioContext and BAIL to the raw mic if the browser won't
give a true 48kHz context (wrong-rate data -> static)
- force the worklet node to explicit mono in/out
- use the non-SIMD rnnoise.wasm (SIMD build artifacts on some GPUs)
- share one AudioContext across captures
Also fix the two CI-blocking eslint errors (unused vars in UrlPreviewCard
and useLocalMessageSearch) and apply repo-wide prettier formatting so
check:eslint and check:prettier pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- usePresenceUpdater: replace stale closure with readStatus() called at
invocation time so changing custom status in Profile Settings is never
silently overwritten by subsequent activity events
- CallEmbedProvider: fix m.space.parent state-key lookup by switching
getStateEvent → getStateEvents (plural); space channel voice rooms no
longer trigger the incoming-call ring/animation
- Add useUserNotes hook (io.lotus.user_notes account data, reactive via
useAccountDataCallback, 500-char limit, cross-device sync)
- UserRoomProfile: add UserPrivateNotes textarea with 800ms debounced
auto-save, saving indicator, char counter when <100 chars remain;
shown only when viewing another user's profile
- LOTUS_FEATURES.md: add Private Notes section, Status Revert fix note,
animation improvements subsection, Seasonal Themes section
- LOTUS_BUGS.md: mark presence revert + voice ringing bugs as resolved
- README.md + landing/index.html: document all new June 2026 features
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Status message: Synapse clears status_msg when a user goes offline/reconnects.
Fix by caching to localStorage and re-sending on setOnline(). The sync
effect no longer overwrites the local value with an empty server event.
Timezone: PUT /profile/{userId}/m.tz is MSC1769 (unstable) and not
supported by standard Synapse — save/load silently fails. Fix by using
Matrix account data (im.lotus.timezone) instead, which is fully
supported. Own profile falls back to account data; other users still
try the m.tz profile endpoint (for federated servers that support it).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove status_msg from setOnline/setUnavailable so server preserves
any custom status message the user has set
- Fix manifest icons array paths (./public/android/ -> ./res/android/)
Junior had it backwards: shortcut icon was correct, main icons were wrong
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- useFileDrop: reset drag overlay when mouse leaves browser window
(relatedTarget === null signals viewport exit, counter was getting stuck)
- useDeviceVerificationStatus: add member count to useMemo deps so new
room members' devices get checked, not just initial joined members
- index.css: define --bg-surface-variant used by VoiceMessageRecorder,
MessageSearch, SearchFilters, UserRoomProfile (was falling back to transparent)
- syntaxHighlight: fix Python inline comments — # after space/tab was
treated as plain text; only start-of-line was recognised
- usePresenceUpdater: replace internal baseUrl cast with mx.getHomeserverUrl()
- useLocalMessageSearch: scan all linked timelines via getUnfilteredTimelineSet()
not just the live window, so scrolled-back history is included in E2EE search
- RoomViewHeader: show search button in encrypted rooms — local search is
implemented and handles them; the guard was a holdover from before it existed
- recent-emoji: return emojis in recency order (array is already unshifted on
use) instead of sorting by total usage count
Skipped: media gallery memory leak (needs virtualization refactor),
bookmark race condition (needs queue/lock), Night Light portal coverage
(position:fixed already covers full viewport — not a real bug).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass status_msg: '' explicitly on setOnline/setOffline/setUnavailable(idle)
so the Matrix server overwrites the 'dnd' status_msg left from DND mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a manual presence picker to the sidebar user avatar. Clicking the
avatar opens a popout menu with Online, Idle, Do Not Disturb, Invisible,
and Auto (activity-based) options. The selected status is shown as a
colored badge on the avatar and stored in settings (survives reloads).
usePresenceUpdater now short-circuits for manual states and only runs
the full activity-tracking logic in Auto mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Announce online immediately on app startup
- Idle detection: unavailable after 10 min of no input, online on return
- Tab visibility: unavailable when hidden, online when focused again
- Page close: offline via fetch+keepalive (survives unload without bfcache penalty)
- hidePresence setting: broadcasts offline and stops all tracking
- Added 'Hide Online Status' toggle in General settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>