3898 Commits
Author SHA1 Message Date
nathan 3f37f69a13 i hate npm audit
CI / Build & Quality Checks (pull_request) Successful in 11m36s
CI / Trigger Desktop Build (pull_request) Skipped
2026-08-02 22:21:32 -04:00
nathan be004411c3 added another package back in
CI / Build & Quality Checks (pull_request) Successful in 10m51s
CI / Trigger Desktop Build (pull_request) Skipped
2026-08-02 21:58:24 -04:00
nathan f207da2344 fix ci prettier mistake
CI / Build & Quality Checks (pull_request) Successful in 10m57s
CI / Trigger Desktop Build (pull_request) Skipped
2026-08-02 21:39:17 -04:00
nathan b90c3b7313 Merge remote-tracking branch 'origin/lotus' into sw-fix
CI / Build & Quality Checks (pull_request) Failing after 6m7s
CI / Trigger Desktop Build (pull_request) Skipped
2026-08-02 21:10:35 -04:00
nathan c8475f07aa added restriction to lint only ts,js files
CI / Build & Quality Checks (pull_request) Failing after 5m57s
CI / Trigger Desktop Build (pull_request) Skipped
2026-08-02 21:03:43 -04:00
nathan 9b10d7d1c2 added prettier auto-fix to pipeline
CI / Build & Quality Checks (pull_request) Canceled after 1m19s
CI / Trigger Desktop Build (pull_request) Canceled after 0s
2026-08-02 21:02:01 -04:00
nathan a1bdaf65f6 removed unused eslint-disables, ran prettier fix
CI / Build & Quality Checks (pull_request) Canceled after 0s
CI / Trigger Desktop Build (pull_request) Canceled after 0s
2026-08-02 21:00:25 -04:00
nathan 0656e6ecb0 removed eslint unused ignore lines, config handles ignoring it now
CI / Build & Quality Checks (pull_request) Canceled after 0s
CI / Trigger Desktop Build (pull_request) Canceled after 0s
2026-08-02 20:55:42 -04:00
nathan 89ba05f462 removed warning for 'any' as a type from eslint
CI / Build & Quality Checks (pull_request) Canceled after 0s
CI / Trigger Desktop Build (pull_request) Canceled after 0s
2026-08-02 20:49:34 -04:00
jaredandClaude Opus 4.8 d5cfb663b9 chore(issues): point the issue chooser at this repo, not upstream Cinny
CI / Build & Quality Checks (push) Successful in 10m54s
CI / Trigger Desktop Build (push) Successful in 5s
The forked-in Cinny issue setup funneled reporters to the upstream project:
- config.yml disabled blank issues and linked to cinnyapp/cinny GitHub
  Discussions
- preapproved.md was Cinny's "do not open an issue, use Discussions"
  template (Gitea has no Discussions anyway)

Replace it with a Lotus setup: enable blank issues, drop the upstream
contact link, and add Bug Report + Feature Request templates. Now
/issues/new/choose offers Lotus templates and files against this repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 20:48:59 -04:00
nathan 2d5e90f715 linter doesnt like void
CI / Build & Quality Checks (pull_request) Canceled after 0s
CI / Trigger Desktop Build (pull_request) Canceled after 0s
2026-08-02 20:48:08 -04:00
nathan f1e4ef657c removed commitizen, added eslint package back
CI / Build & Quality Checks (pull_request) Canceled after 0s
CI / Trigger Desktop Build (pull_request) Canceled after 0s
2026-08-02 20:46:11 -04:00
nathan c1ec68e73e Merge remote-tracking branch 'origin/lotus' into sw-fix
CI / Build & Quality Checks (pull_request) Canceled after 2m37s
CI / Trigger Desktop Build (pull_request) Canceled after 0s
2026-08-02 20:30:14 -04:00
f2c356f288 refactor(assets): build logo/favicon URLs via withOriginBaseUrl helper
CI / Build & Quality Checks (push) Successful in 10m37s
CI / Trigger Desktop Build (push) Canceled after 0s
Swap the logo/favicon URL constants from the inline
`${trimTrailingSlash(import.meta.env.BASE_URL)}/public/res/...` form to the
repo's existing `withOriginBaseUrl(getOriginBaseUrl(), '/public/res/...')`
helper (already used here for the OIDC callback URL). Functionally equivalent —
same /public/res/ target, resolves in dev and the static-copied prod build — and
it keeps the logo URL absolute and consistent with clientUri for the OIDC
logoUri. No build-config change (publicDir stays false).

Co-authored-by: Nathan Vititoe <nathanvititoe@gmail.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 20:22:58 -04:00
nathan f6d66af280 Merge remote-tracking branch 'origin/image-path-changes' into sw-fix
CI / Build & Quality Checks (pull_request) Failing after 6m1s
CI / Trigger Desktop Build (pull_request) Skipped
2026-08-02 20:06:44 -04:00
nathan 0aef410f60 fixed retarded prettier 'error'
CI / Build & Quality Checks (pull_request) Successful in 10m45s
CI / Trigger Desktop Build (pull_request) Skipped
2026-08-02 19:01:05 -04:00
nathan 50bb509484 fixed retarded prettier 'error'
CI / Build & Quality Checks (pull_request) Failing after 6m2s
CI / Trigger Desktop Build (pull_request) Skipped
2026-08-02 19:00:41 -04:00
nathan b2376513fd fixed retarded linter problem
CI / Build & Quality Checks (pull_request) Failing after 6m2s
CI / Trigger Desktop Build (pull_request) Skipped
2026-08-02 18:42:37 -04:00
nathan 149ea96957 removed test change
CI / Build & Quality Checks (pull_request) Failing after 6m1s
CI / Trigger Desktop Build (pull_request) Skipped
2026-08-02 18:37:30 -04:00
nathan 093f10db5c fix problem registering service worker when in a development environment
CI / Build & Quality Checks (pull_request) Canceled after 1m37s
CI / Trigger Desktop Build (pull_request) Canceled after 0s
2026-08-02 18:34:54 -04:00
nathan 6a0037ecec read me updates with instructions for running a dev server
CI / Build & Quality Checks (pull_request) Failing after 5m54s
CI / Trigger Desktop Build (pull_request) Skipped
2026-08-02 18:04:25 -04:00
nathan.vititoe 2bbd390a3b image path changes, for dev setup, needs testing on 'prod'
CI / Build & Quality Checks (pull_request) Failing after 6m0s
CI / Trigger Desktop Build (pull_request) Skipped
2026-08-02 16:35:11 -04:00
jaredandClaude Opus 4.8 f12e05c510 fix(assets): reference logo/favicon PNGs by URL instead of importing from public/
CI / Build & Quality Checks (push) Successful in 12m7s
CI / Trigger Desktop Build (push) Successful in 7s
Importing images from public/ (`import X from '../../public/res/*.png'`) is
fragile under Vite with `publicDir: false` and can white-screen the dev app —
Vite tries to resolve the public/ path as a module. Switch the five logo/
favicon call sites to the repo's existing BASE_URL URL pattern
(`${trimTrailingSlash(import.meta.env.BASE_URL)}/public/res/...`), matching how
config.json, locales, and the Element Call widget are referenced. Resolves
identically in dev and the static-copied prod build (public/res -> dist/public/res).

Sounds under public/sound/ stay ESM-imported: that folder is not copied to
dist/ by vite-plugin-static-copy, so a URL reference would 404.

Also add a Local Development section to the README (no-backend model, npm ci /
npm start on :8080, which homeserver to log in against, OIDC-on-localhost note).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 15:52:04 -04:00
jaredandClaude Opus 4.8 d47032a14f fix(unread): clear rooms whose read receipt already covers the tail
CI / Build & Quality Checks (push) Successful in 10m54s
CI / Trigger Desktop Build (push) Successful in 8s
A room could show a permanent unread that survives every cold start even
though the server considers it fully read (notification_count 0, unthreaded
read receipt at the tail). matrix-js-sdk's fixNotificationCountOnDecryption
only ever INCREMENTS an encrypted room's Total, and addReceipt's auto-clear
fires only when the tail event is the user's own — so a count inflated in an
earlier state (before a receipt covered the tail, e.g. by a since-corrupted
undecryptable event) is never decremented and keeps a genuinely-read room lit.
This is aggravated by mixing threaded-receipt clients (Element X) with
unthreaded ones (Lotus/Cinny), which split the read marker.

Add readReceiptCoversTail(room, userId): walking the live timeline newest→
oldest, if we reach the user's read-receipt event without crossing any
notification-worthy event, the room is genuinely read and a lingering Total is
suppressed to {0,0} in getUnreadInfo / getUnreadInfos. Safe by construction —
a real unread sits AFTER the receipt and stops the walk at isNotificationEvent
— and guarded against unread threads (markAsRead clears threads unconditionally)
and off-window receipts (can't confirm → don't suppress). Self-correcting: a
new message becomes the tail and the walk stops suppressing.

Also recognize polls (m.poll.start / msc3381) as notification events so a
poll-only unread is never walked past (closes a pre-existing gap in the
tail scans), and factor the unread-thread guard into roomHasUnreadThread.

Reviewed by 3 agents (false-suppression safety, unread-system regression,
SDK behavior): no real unread is hidden for any standard content, no
regression to the atom/PUT-DELETE paths, and the fix produces {0,0} for the
target scenario and stays resolved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 18:42:32 -04:00
jaredandClaude Opus 4.8 b2678d5c6d fix(unread): stop DM device-verification requests re-lighting as unread
CI / Build & Quality Checks (push) Successful in 11m11s
CI / Trigger Desktop Build (push) Successful in 10s
A completed in-room device-verification request is a plain m.room.message
(msgtype m.key.verification.request) that matches the default DM push rule
with no recency gate, so the server/SDK notification count stays > 0 and the
DM re-lights as unread on every fresh sync until the room is opened twice.

Two-part fix:
- Display suppression: getUnreadInfo/getUnreadInfos return {0,0} for a room
  whose ENTIRE unread span (tail -> read receipt) is verification-flow events,
  via new pure helpers isVerificationFlowEvent + unreadIsOnlyVerification.
  Conservative: never suppresses when the read marker is off-window, the tail
  is still encrypted, or a highlight is present.
- Durable auto-read: useAutoMarkVerificationRead sends a read receipt covering
  the request (the only SDK-durable lever), once per room per session, gated on
  the same verification-only predicate so it can never ack a real message.

unreadIsOnlyVerification also rejects any room with an unread thread, because
markAsRead clears every thread unconditionally — otherwise a verification-only
main timeline with a genuine unread thread reply would be hidden/auto-acked.

Reviewed by 5 agents; the thread-scope guard closes the one bug they found.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 01:18:31 -04:00
jaredandClaude Opus 4.8 1176bea0ee docs(todo): record composer autocomplete-insert crash fix (477df4ae)
CI / Build & Quality Checks (push) Successful in 10m45s
CI / Trigger Desktop Build (push) Successful in 7s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 16:13:34 -04:00
jaredandClaude Opus 4.8 477df4ae32 fix(composer): stop the intermittent autocomplete-insert crash
Picking an autocomplete item (mention/emoji/command — all inline voids)
occasionally tripped the composer error boundary, forcing a page refresh, even
though the element had already inserted. Root cause (traced through slate-react):
moveCursor deferred its cursor work to setTimeout(0), leaving the caret on the
just-inserted void's zero-width edge whose DOM (a U+FEFF node) isn't populated on
that tick. slate-react's commit-phase selection sync then calls
setBaseAndExtent(voidEdge, 1) and throws IndexSizeError mid-render → boundary.

Prevention: do the cursor work SYNCHRONOUSLY, in the same commit as the insert —
Transforms.move (escapes the void into the real trailing text node) then
insertText(' '). The caret is then always a resolvable text point when the
selection sync runs. (moveCursor's focus stays deferred+guarded, unchanged.)

Recovery (belt-and-suspenders): the composer error boundary is now recoverable —
a "Reload composer" button (resetErrorBoundary) + onReset Transforms.deselect
clears a transient bad selection so it remounts with the draft intact, no page
refresh. + role="alert" for screen readers.

Three review agents: two root-caused the exact slate-react throw and proved the
try/catch-only version merely recovered; a third reproduced the transforms
headlessly and caught that a first "sync insertText WITHOUT move" attempt hit
Slate's void guard (space dropped, caret trapped) — the move is required to
escape the void. Not unit-testable (needs the live DOM + the timing race).
Gate-green (tsc, eslint, prettier, 925 tests, build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 16:12:52 -04:00
jaredandClaude Opus 4.8 15d85f52c4 docs(todo): desktop notification nav fix + rich-toast follow-ups (0ddf86c6)
CI / Build & Quality Checks (push) Successful in 10m51s
CI / Trigger Desktop Build (push) Successful in 8s
Root-caused the desktop notification-click-doesn't-navigate bug (SW shadowed the
Notification shim); web fix shipped. Documented the two desktop-Rust follow-ups
it activates (lost tag-coalescing, thread/invite quick-reply misroute) + a
Windows QA checklist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 20:17:39 -04:00
jaredandClaude Opus 4.8 0ddf86c678 fix(desktop): navigate to the message on notification click (route via rich toast)
On the Windows/Tauri build, clicking a message notification opened the app but
didn't navigate to the message. showOsNotification preferred the service worker
(registration.showNotification) and returned early; WebView2 has a service
worker, so the SW-owned toast always won and its click (SW notificationclick →
client.focus + postMessage → navigate) focused the app but the navigate didn't
complete in WebView2.

The desktop build injects a window.Notification shim that routes tagged message
toasts to the native rich WinRT toast, whose click emits lotus-notification-
activate with the path → useTauriToastActions → navigate. But the SW path
shadowed `new Notification()`, so that shim (and show_rich_toast) never ran on
desktop. Skipping the SW path under Tauri lets the shim take over and navigate.

Web browsers are unchanged (isDesktopApp() is false → SW path as before). Two
review agents verified the diagnosis + no web regression across both repos.

DESKTOP-QA REQUIRED — this activates a previously-dead code path. Known desktop
follow-ups it exposes (documented in LOTUS_TODO, both in cinny-desktop Rust):
- tag-coalescing is lost (rapid same-room messages stack toasts instead of
  collapsing) — show_rich_toast doesn't dedupe by room.
- thread/invite quick-reply misroutes: the reply target is the coalescing tag
  (roomId:threadId / 'lotus-invites'), not a real room id → sendMessage fails.
Navigation itself (body click) is correct for all cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 20:16:55 -04:00
jaredandClaude Opus 4.8 bd5f6a0855 docs(todo): mark quiet-hours empty-time feedback fixed
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:59:50 -04:00
jaredandClaude Opus 4.8 5175c095b7 fix(notifications): flag inactive quiet-hours when a time field is empty
Clearing the quiet-hours start or end time silently deactivated the window
(isWithinTimeWindow → parseHHMM('') is null → returns false) while the toggle
still read "on", with no indication. Added an inline Critical-colored hint —
"Set both a start and end time — quiet hours stay inactive until both are filled
in" — shown when the toggle is on but either field is empty. Non-destructive:
it explains why rather than guessing a default time. Copy verified against
isWithinTimeWindow.

Last pure-client bug-hunt finding from LOTUS_TODO (the rest are live-call /
desktop-gated). Gate-green (tsc, eslint, prettier, build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:59:50 -04:00
jaredandClaude Opus 4.8 99629edd9c docs(todo): mark MLocation permalink + PolicyListViewer doc findings fixed (8a461610)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:57:48 -04:00
jaredandClaude Opus 4.8 8a461610f4 fix(low-tail): MLocation permalink uses validated floats; PolicyListViewer doc
- MLocation "Open in OpenStreetMap" permalink built its URL from the raw
  parseGeoUri strings (location.latitude/longitude) while the embedded map iframe
  used the parseFloat + isFinite-validated lat/lon. Use lat/lon in the permalink
  too, so a malformed geo: substring can't reach the URL (they're already proven
  finite a few lines above and used identically in mapSrc).
- LOTUS_FEATURES claimed the Policy List Viewer has "Subscribe (join) /
  unsubscribe (leave) controls for each list" and lists subscribed lists.
  Verified against PolicyListViewer.tsx: it's a room-ID/alias input viewer that
  displays a joined policy room's rules read-only — no subscribe controls, no
  subscribed-lists listing. Corrected the doc to match.

Two low-tail bug-hunt findings from LOTUS_TODO. Gate-green (tsc, eslint,
prettier, build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:57:34 -04:00
jaredandClaude Opus 4.8 53a2f738a9 docs(todo): mark pip auto-spotlight release finding fixed (08e19100)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:54:48 -04:00
jaredandClaude Opus 4.8 08e191008b fix(call): release auto-enabled pip spotlight when returning to the call room
In picture-in-picture with an active screenshare, spotlight is auto-enabled so
the share fills the pip window (tracked via pipAutoSpotlightRef). The release
branch sat behind `if (!pipMode) return`, so navigating BACK to the call room
(pipMode → false) early-returned and never released it — the spotlight stayed
stuck on with the ref latched true.

The effect now guards only on `!callEmbed`, computes wantSpotlight = pipMode &&
pipScreenshare, and releases whenever that's false (screenshare ends OR pip
ends). The ref still gates release so we only ever undo a spotlight we enabled,
never the user's. Two reviewer-prescribed hardenings folded in: reset the ref
when callEmbed is torn down (kills a stale cross-call latch), and a comment that
control.spotlight is deliberately not a dep (re-adding it would fight the user).

Bug-hunt finding from LOTUS_TODO. Two review agents verified against
CallControl.ts (ref-gating, deps, idempotency, cross-embed self-heal); [live] —
the code fix is unambiguous but confirming screenshare→pip→back wants a real
call. Gate-green (tsc, eslint, prettier, 925 tests, build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:54:29 -04:00
jaredandClaude Opus 4.8 654466cf45 docs(todo): mark export-history E2EE pagination finding fixed (3ff8fb8e)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:47:53 -04:00
jaredandClaude Opus 4.8 3ff8fb8e55 fix(export): advance the raw pagination boundary on every event
Exporting a date range from an ENCRYPTED room over-paginated and mislabeled
"truncated": oldestRawTs (the how-far-back-have-we-paged boundary) was updated
only after the RoomMessage + decryption-failure filters, so undecryptable or
non-message old events never advanced it, the fromTs break never fired, and the
loop ran to MAX_EXPORT_PAGES. getTs() is unencrypted envelope metadata, so the
boundary update now runs for every event, above the filters.

Guarded with `ts > 0` so a bogus 0/negative origin_server_ts can't collapse the
boundary and cause the opposite failure — a silent early break / under-paginated
export (per review, silent omission in an export is worse than the loud
over-pagination this fixes). oldestTs (oldest collected in-range message) is
unchanged.

Two review agents (both confirmed getTs is decryption-independent, no
intra-page collection regression, oldestRawTs feeds only the fromTs break, no
plaintext regression); the second surfaced the 0-ts under-pagination edge, hence
the guard. Not unit-testable (embedded component + needs an E2EE room with
undecryptable history). Gate-green (tsc, eslint, prettier, 925 tests, build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:47:40 -04:00
jaredandClaude Opus 4.8 02089cf60e docs(todo): mark encrypted-search cache size-cap finding fixed (fff811cb)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:41:40 -04:00
jaredandClaude Opus 4.8 fff811cb2d fix(search): cap the encrypted-search IDB cache per room (bound disk growth)
The on-device search index grew unbounded over a long session. putRows now
prunes each touched room to MAX_ROWS_PER_ROOM (5000) — deleting the oldest rows
by [roomId, ts] via a self-chaining IDB cursor within the same write tx (never
awaits a non-IDB promise mid-tx, so the transaction can't auto-commit and
truncate the prune). Exposed a pure, unit-tested evictCount() for the decision;
the cursor path itself is browser-only (node --test has no IndexedDB).

Deliberate tradeoff (documented in code): the coverage window keeps claiming the
evicted tail so the search doesn't re-fetch → re-evict it forever. Net effect —
in a room past 5000 cached rows, an evicted old message is silently unsearchable
rather than churning. Clear cached index / logout still wipe everything.

Two review agents verified the IndexedDB-spec correctness (cursor delete+continue
semantics, put-then-count ordering, roomRange bracketing with no prefix bleed,
tx liveness, abort→cache-miss) since CI can't. Gate-green (tsc, eslint, prettier,
925 tests, build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 19:41:16 -04:00
jaredandClaude Opus 4.8 f54c386f36 docs(testing): add an automated-coverage map to the manual QA guide
Maps manual test items to the unit tests that now pin their LOGIC, so a human
tester can trust the deterministic parts and spend manual time on the
genuinely-human surface (visual rendering, live calls, desktop build, E2EE,
cross-device sync). Each row splits "logic pinned by a unit test" from "what
still needs you".

Every row verified against the real test assertions, then independently
audited by an agent for overclaims — the important failure mode being a tester
skipping manual QA of something not actually tested. Audit-driven corrections:
- O4 search cache: the IndexedDB round-trip test is skip'd under `npm test`
  (node has no IndexedDB), so only the pure merge/coverage helpers run in CI —
  said so explicitly rather than implying the round-trip is CI-covered.
- F2: relabeled — seasonSchedule.test.ts pins seasonal-theme *resolution*, NOT
  F2's background↔seasonal mutual exclusion (which is untested); flagged so no
  one skips the real F2 behavior.
- O5 + Q1/Q2: widened to reflect coverage that was understated.

No dangerous overclaim survived; the visual/live/device/E2EE carve-outs hold
for every row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 18:57:42 -04:00
jaredandClaude Opus 4.8 6dc0865965 docs(todo): mark soundboard-timer + permission-listener findings fixed (56561627)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:43:19 -04:00
jaredandClaude Opus 4.8 5656162720 fix(call): clear soundboard safety timer + detach permission onchange
- CallSoundboard: the 30s safety timeout (which unsticks the one-at-a-time
  playingKey guard if audio never signals end) was never cleared, so it fired
  ~30s after every clip. It's now stored in a per-play token that done() clears
  by identity — a natural 'ended' cancels it, and a stale done() from a prior
  clip can't disarm a newer clip's timer (which matters because a rejected
  audio.play() fires neither ended nor error, leaving the timer as the only
  guard-reset). The unmount effect also clears any pending timer, and the timer
  is armed only when there's an audio element.

- PrescreenControls: useMediaPermissions set PermissionStatus.onchange but never
  removed it → a permission change after unmount setState'd a dead component and
  retained the callback. Now guards all setState with a cancelled flag and
  detaches onchange in the effect cleanup.

Bug-hunt findings from LOTUS_TODO. Three review passes (the last prescribed the
per-play token to close a shared-ref cross-play edge). Gate-green (tsc, eslint,
prettier, 922 tests, build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:43:04 -04:00
jaredandClaude Opus 4.8 c6d558e5dd docs(todo): mark seasonal auto-ticker + mutual-exclusion findings fixed (d416c62b)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:35:07 -04:00
jaredandClaude Opus 4.8 d416c62b4c fix(seasonal): auto theme re-evaluates over time; auto clears chat background
- The "auto" seasonal theme was computed once at mount, so a long-lived session
  never crossed a season/holiday-window boundary. SeasonalEffect now re-evaluates
  on an hourly ticker (auto mode only) AND refreshes on entering auto — the
  interval only runs while auto, so a stale mount-time timestamp would otherwise
  resurface on a pinned/off → auto switch (the exact frozen-at-mount bug, caught
  in review). The decision is extracted to a pure resolveSeasonTheme(override,
  now) in seasonSchedule.ts (removing an unsafe cast) and unit-tested.

- Selecting seasonal "auto" while a chat background was set was a silent no-op:
  the seasonal picker only cleared the background for a *specific* theme, and the
  overlay is suppressed while a background is set. Now any active seasonal mode
  ("auto" included) clears the background; only "off" leaves it — symmetric with
  the background picker (which sets seasonal "off"). The overlay guard stays as a
  backstop for legacy persisted state.

Bug-hunt findings from LOTUS_TODO. Three review passes (the 2nd caught the
switch-into-auto staleness); +2 unit tests. Gate-green (tsc, eslint, prettier,
922 tests, build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:34:51 -04:00
jaredandClaude Opus 4.8 a24c98b199 docs: reconcile tab-title + collapsible-message threshold claims with code
Two bug-hunt findings that were doc inaccuracies, not code bugs:
- Tab title "(N)" is the mention/highlight count (+ "·" for other unread),
  mirroring the favicon — intentional. LOTUS_FEATURES said "N unread messages";
  corrected to describe the actual highlight-count behavior.
- Collapsible long messages use a fixed COLLAPSE_MAX_HEIGHT (320px ≈ 20 lines);
  the doc claimed a Settings → Appearance control that never existed. Corrected
  to describe the fixed threshold rather than build a marginal per-user setting.

Verified against ClientNonUIFeatures.tsx and MsgTypeRenderers.tsx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:24:58 -04:00
jaredandClaude Opus 4.8 8fbde6df36 docs(todo): mark toast-cap + unread-sort findings fixed (1963222d)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:22:40 -04:00
jaredandClaude Opus 4.8 1963222d1e fix(ux): cap the in-app toast stack; stable "Unread First" room sort
- Toast queue: a burst of notifications appended unboundedly and could cover the
  viewport. Cap at 5 in the atom writer, dropping the OLDEST non-sticky toast
  (sticky = action toasts requiring a click, never dropped). The drop scan
  excludes the just-appended newest (`length - 1` bound) so a fresh toast is
  never the one eaten when the cap is full of stickies — it stretches instead.
  Container gains a maxHeight + overflowY safety net and scrolls the newest
  (bottom) toast into view if the stack ever overflows. +4 unit tests incl. the
  cap-full-of-stickies boundary.

- "Unread First" room sort left the entire read tail (all counts tie at 0) in
  arbitrary Map order. factoryRoomIdByUnread now breaks ties by recent activity.
  Relocated from Home.tsx (module-private) to utils/sort.ts (exported, pure) and
  unit-tested (equal-count and read-tail cases fall back to activity).

Bug-hunt findings from LOTUS_TODO. Three review passes: the second caught that
the cap could silently drop the newest notification when full of stickies (real
bug, untested boundary) — fixed and covered; a third traced the corrected loop.
Gate-green (tsc, eslint, prettier, 920 tests, build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:22:23 -04:00
jaredandClaude Opus 4.8 d07f16586a docs(todo): mark push-rule + MSC1929-support findings fixed (2c0cd0d2)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:06:19 -04:00
jaredandClaude Opus 4.8 2c0cd0d26c fix(settings): resync push-rule toggle from account-data; MSC1929 support host
- PushRuleEditor: the enable Switch initialized its state from pushRule.enabled
  once (useState initializer), so a rule toggled on another device left the
  Switch stale until remount. A useEffect now resyncs on pushRule.enabled
  change. pushRule flows from useAccountData(m.push_rules), which re-renders on
  sync, so the resync is genuinely reached; no optimistic-update conflict (the
  toggle sets state only after the PUT resolves).

- About: the "Homeserver Support" panel fetched /.well-known/matrix/support from
  the client-API URL (mx.getHomeserverUrl()). Per MSC1929 that file lives at the
  MXID server-name host (like /.well-known/matrix/client), which differs on
  delegated/split-domain servers. Now fetched from https://{mx.getDomain()};
  identical target for non-delegated servers (incl. Lotus), spec-correct for
  delegated ones, and degrades gracefully (catch → panel hidden) otherwise.

Bug-hunt findings from LOTUS_TODO. Two review agents; both confirmed effective
and non-regressing (full account-data re-render chain traced; CORS/host edge
weighed). Gate-green (tsc, eslint, prettier, 914 tests, build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 17:00:36 -04:00
jaredandClaude Opus 4.8 b0a3c81b15 docs(todo): mark 6 bug-hunt findings fixed (decorations, presence, denoise)
Closes the avatar-decoration cluster (live-update pub/sub, CDN-override
thumbnail, profile-404), the DND badge color, and the DenoiseTester
model-node leak + async mounted-guard — commits 29ff1654 and c9d9d914.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 16:44:44 -04:00