Screen readers now announce new messages politely via role='log' +
aria-live='polite' on the message container in RoomTimeline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two icon-adjacent buttons were missing descriptive labels: the
"Exit formatting" key-symbol button in Toolbar.tsx and the "Pinned
messages" pin icon in RoomViewHeader.tsx.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds objectPosition:'center top' to all cover-fit thumbnail surfaces so
portrait images show faces/subjects instead of the center-slice when
the 600px AttachmentBox height cap forces cropping.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MediaGallery: fixed panel now goes full-width (100%) on mobile instead
of the inaccessible 320px right sidebar. Added 'Media Gallery' MenuItem
to RoomMenu (visible only on mobile) so users can open it from the
More Options (···) button.
MembersDrawer: removed ScreenSize.Desktop gate in Room.tsx so it now
renders on mobile too. CSS media query (≤750px) makes it position:fixed
inset:0 width:100% on mobile instead of the 266px desktop sidebar.
Added 'Members' MenuItem to RoomMenu for mobile access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds enabled=true param to useDecryptedMediaUrl in MediaGallery.tsx.
GalleryTile now uses useNearViewport(300px) — decryption is deferred
until the tile approaches the viewport, preventing burst of 100
concurrent decrypt/fetch calls when a pagination batch loads.
Blob revocation was already correct (no actual leak); this fixes the
load-burst performance issue. Full windowing virtualization deferred.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Completes the mobile fullscreen modal pass — adds useModalStyle to
DeviceVerificationSetup, DeviceVerificationReset, AddExistingModal,
RoomEncryption prompt, RoomUpgradeDialog, Modal500, ReadReceiptAvatars,
and RoomTopicViewer. All floating Dialog/Modal components now go
fullscreen on mobile (≤750px). UIAFlowOverlay was already fullscreen
via <Overlay>; JoinRulesSwitcher/RoomNotificationSwitcher are dropdowns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TauriUpdateFeature component in ClientNonUIFeatures checks for updates
on mount and every 12h (skips if checked within the window). On update
available, fires a Lotus toast: "Lotus Chat vX.Y.Z is ready to install."
Clicking the toast calls install(). No-op on web (isTauri guard).
Also adds optional onClick to ToastNotif type and wires it in
LotusToastContainer so custom click handlers can skip hash navigation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- matrix.ts: rateLimitedActions fallback delay now uses capped exponential
backoff (min(1000 * 2^n, 30s)) instead of flat 3000ms when server omits
Retry-After; server header still takes precedence
- RenderMessageContent: add objectFit:cover + 100% fill to video thumbnail
<Image> so thumbnails fill their container without letterboxing (P5-6)
- CreateRoomModal, CreateSpaceModal: apply useModalStyle(480) for fullscreen
on mobile
- LOTUS_BUGS: mark usePan memory leak + httpStatus check as FALSE POSITIVE;
mark rateLimitedActions backoff as FIXED
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On mobile, SidebarNav (which contains BookmarksTab) is hidden while
inside a room. Added a Saved Messages toggle to the RoomMenu (···More
Options) so users can open/close the bookmarks panel without leaving
the room. Works on all screen sizes; UNTESTED on device.
Also marks Remind Me Later as done in LOTUS_TODO — it was already
fully wired (RemindMeDialog + ReminderMonitor + Message.tsx trigger).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ScheduleMessageModal, PollCreator, JoinAddressPrompt, JumpToTime,
EditHistoryModal, ForwardMessageDialog, RemindMeDialog — all now
render fullscreen on mobile (≤750px) and as a capped-width box on
desktop, consistent with the existing useModalStyle pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bug #10: use `100dvh` on <html> so layout shrinks when mobile virtual keyboard
appears (prevents composer from being pushed off-screen)
- Bug #7: add `minWidth/minHeight: 44px` to all 8 composer toolbar IconButtons on
mobile via mobileOrTablet() check (WCAG 2.1 AA touch target requirement)
- Bug #8: add `@media (max-width: 750px) { width: 100% }` to PageNav recipe
variants so the nav panel fills full width on mobile instead of overflowing
with its fixed desktop width
- Bug #9: introduce `useModalStyle(maxWidth)` hook — returns fullscreen styles on
mobile (no border-radius, no max-width cap, height 100%) and desktop box styles
otherwise; applied to LeaveRoomPrompt, LeaveSpacePrompt, ReportRoomModal,
ReportUserModal
- Bug #11: mark as FALSE POSITIVE in LOTUS_BUGS.md — `useState(() => atom(...))`
is the correct Jotai pattern for stable local atom references
- Scheduled Messages persistence: mark as FIXED — already uses atomWithStorage +
createJSONStorage with error-safe JSON parsing
- UrlPreviewCard TDS colors: mark as BRAND EXCEPTION — SVG logo fills and site
badge backgrounds are official third-party brand colors; cannot convert without
inventing new CSS variables (violates TDS rule 3)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ReportUserModal.tsx — category dropdown + reason input, calls
POST /_matrix/client/v3/users/{userId}/report via mx.http.authedRequest,
inline success/error feedback, auto-closes 1500ms after success
- Wire Report User button into UserRoomProfile.tsx between UserModeration
and UserDeviceSessions (hidden for own profile)
- Bug #6: enforce mutual exclusion between chat backgrounds and seasonal
themes — ChatBgGrid clears seasonal→'off' on non-'none' pick;
SeasonalBgGrid clears chatBackground→'none' on real theme pick;
SeasonalEffect guards against legacy persisted state at render time
- TDS: strip all hardcoded hex/rgba fallbacks from LotusToastContainer.tsx
(var(--lt-bg-card), --lt-accent-orange, --lt-text-primary/secondary,
--lt-accent-orange-dim/border, --lt-box-glow-orange)
- Mark Bug #6 FIXED, MSC4260 DONE, toast TDS FIXED in LOTUS_BUGS.md and
LOTUS_TODO.md; note EventReaders + CallControls already compliant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace raw CSS variables and Button presets with folds color/config
tokens and MenuItem components, matching the ForwardMessageDialog and
ScheduleMessageModal patterns exactly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move the model comparison out of the always-visible Noise Suppression
description and into the ML-only sub-settings. Add a compact info card
for the selected model (CPU / voice quality / transients / download) plus
a collapsible 4-model comparison. Group ML sub-settings into Model,
Enhancements, and Test & calibrate sections with clear labels and
separators. Fix invented --lt-border-color token and hardcoded
rgba background to real TDS tokens. Build the model dropdown and
DenoiseTester labels/compare buttons from DENOISE_MODELS so
DeepFilterNet 3 is handled correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Integrate DeepFilterNet 3 (deepfilternet3-noise-filter@1.2.1) as a new
client-side denoise model id 'deepfilternet', mirroring the DTLN pattern.
The npm package ships only an ESM whose AudioWorklet processor + wasm-bindgen
glue are inlined as a string (loaded via a Blob URL — no CDN for the worklet).
Its only runtime fetches are a single-threaded df_bg.wasm and an ONNX model
tarball, which previously loaded from an external CDN. We now VENDOR both
(build/denoise-vendor/deepfilternet/v2/...) and self-host them under
denoise/deepfilternet/, overriding the package's cdnUrl so nothing hits the
upstream CDN — keeping it self-hosted / Tauri-CSP safe.
The wasm is single-threaded (no SharedArrayBuffer / atomics / imported shared
memory), so it needs no COOP/COEP cross-origin isolation and runs fine in EC's
non-isolated iframe. Runs at 48 kHz fullband. Any init/runtime failure falls
back to the raw mic, like the other models.
- vite.config.js: copy ESM + vendored wasm/model into the EC denoise dir with a
required-asset guard that aborts the build if any entry is missing.
- build/lotus-denoise.js: 'deepfilternet' branch — dynamic-import the ESM, build
a DeepFilterNet3Core pointed at the self-hosted base, await init, return the
worklet node; 48 kHz; raw-mic fail-safe preserved.
- denoisePipeline.ts: 'deepfilternet' branch for the in-app tester + sampleRate.
- settings.ts: add 'deepfilternet' to DenoiseModelId + getSettings whitelist.
- lotusDenoiseUtils.ts: add the comparison-chart row.
- General.tsx: add the "DeepFilterNet 3 (beta)" dropdown option.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two issues found from real testing of the in-app tester:
1. Raw ≈ RNNoise ≈ Speex sounded identical in Record & compare because the clip
was captured with browser noise suppression ON (the user's native-NS
setting), so "Raw" was already cleaned and the models had nothing left to
remove. Record & compare now captures fully raw audio (noiseSuppression /
AGC / echoCancellation off) so each model's effect on real noise is audible.
(Friends still heard differences in calls — the models work; the test was
feeding them pre-cleaned audio.)
2. DTLN was robotic/choppy/quiet because @workadventure/noise-suppression
targets 16 kHz (AUDIO_CONFIG.sampleRate) and does NOT resample internally,
while we ran it at 48 kHz. Run DTLN's whole graph in a 16 kHz context:
- denoisePipeline: add sampleRateFor(model) (16k for dtln, 48k otherwise);
tester live-monitor + playback contexts use it (bufferSource resamples the
48k clip down for DTLN).
- shim (build/lotus-denoise.js): SAMPLE_RATE is now model-aware, so DTLN is
correct in real calls too (it was previously broken at 48 kHz). The 16 kHz
processed track is still published to LiveKit (WebRTC/Opus resamples).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous "Test Microphone" meter only showed a raw 0-100% level bar — it
never ran the gate or any model, and its scale wasn't dBFS, so it couldn't tell
you which threshold to pick or let you hear the models solo. Replace it with a
real tester that reuses the shipped worklets (/public/element-call/denoise/) in
a main-app AudioContext, mirroring the call pipeline (source -> gate -> model).
- denoisePipeline.ts: shared loader for the RNNoise/Speex flat worklets and the
DTLN @workadventure helper, the noise gate, and a dBFS RMS meter helper.
- DenoiseTester.tsx:
- Live monitor: hear yourself through the selected model (+gate) in real time
(headphones) with In/Out dBFS meters and a threshold marker on the In meter
so the gate value is meaningful to calibrate.
- Record & compare: capture a short clip, then A/B the same audio Raw vs
RNNoise vs Speex vs DTLN.
- Wire it into the ML settings block; remove the old raw-only MicMeter. Use real
TDS tokens (--accent-*, --border-color, --bg-card) instead of the invented
--lt-* names + hardcoded hex the old meter used.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The prior DTLN attempt (89a2321d) broke the build (missing dep, wrong
`cinny/` asset paths) and typecheck (`'dtln'` not in DenoiseModelId), and was
wired against an API the package doesn't expose. @workadventure/noise-
suppression is not a flat AudioWorklet — it's a self-contained ES module whose
processor name is `workadventure-noise-suppression` and which resolves its own
LiteRT WASM + TFLite models via import.meta.url. Driving it by hand-rolled
addModule + processorOptions cannot work.
- Re-add @workadventure/noise-suppression@0.0.4 (package.json + lockfile).
- vite: copy the package's whole dist/ tree intact to
denoise/workadventure/ (preserving assets/ + vendor/litert) so import.meta
resolution works at runtime; fail the build if the entry module is missing.
- shim: for the DTLN model, dynamic-import denoise/workadventure/audio-worklet
.js and use createNoiseSuppressionAudioWorklet(ctx, { bypassUntilReady })
to build the node; RNNoise/Speex keep their direct flat-worklet path. Async
init errors are logged + reported and fall back to the raw mic.
- Restore 'dtln' in DenoiseModelId (+ settings coercion), the model chart, and
the settings dropdown, labelled "(beta)".
DTLN builds and is fully self-hosted, but its in-call audio is UNVERIFIED in
this environment — needs a real-call test. DeepFilterNet stays excluded (CDN
asset loading, incompatible with self-hosting / Tauri CSP).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit/repair of the multi-model denoise work so it actually builds and only
exposes working, self-hosted models.
- Complete the DTLN/DFN3 revert: uninstall @workadventure/noise-suppression
and deepfilternet3-noise-filter (package.json + lockfile), drop the unused
DTLN asset-copy block from vite.config.js (was shipping ~2MB of unused
tflite/wasm), and narrow DenoiseModelId to the bundled models (rnnoise,
speex). Coerce any retired persisted model value back to the default.
- Fix General.tsx CI typecheck failures introduced by the denoise UI: restore
three imports the rewrite deleted (useDateFormatItems, SequenceCardStyle,
useTauriUpdater), add the missing denoise/sound imports, and correct
hallucinated Folds props (Text has no variant/bold; Box uses
alignItems/justifyContent). tsc now passes with 0 errors.
- Harden the vite denoise plugin: required RNNoise/Speex/gate assets and the
shim now fail the build loudly if missing (instead of a silent warn that
shipped a broken ML feature), and the index.html shim injection is verified.
- CI: move the cinny-desktop submodule bump into ci.yml as a `trigger-desktop`
job gated on `needs: build`, and delete the standalone trigger-desktop.yml.
A failing push no longer kicks off the slow Tauri builds in parallel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement a flexible, multi-model noise suppression pipeline for Element Call/LiveKit integration:
- ML Engines: Added support for RNNoise, Speex, DTLN, and DeepFilterNet 3 models.
- Pipeline Architecture: Implemented modular audio processing in lotus-denoise.js, supporting 'Series Suppression' (running browser-native NSNet2 before ML) and a hardware-style Noise Gate.
- UI & UX Enhancements:
- Settings UI: Added model comparison chart with CPU/Quality metadata.
- Tuning: Added Live Microphone Meter for calibrating Noise Gate thresholds.
- Reporting: Added LotusToast system to alert users when ML suppression fails or falls back to raw input.
- Robustness & Quality:
- Capture Fidelity: Removed forced 48kHz capture constraints to allow native-rate capture (solving static issues with high-end audio interfaces).
- Performance: Added WASM SIMD detection with transparent fallback.
- Capability Detection: Added browser feature detection to disable unsupported ML modes.
- Build Integration: Updated Vite config to self-host all model WASM/tflite assets in /denoise/ directory.
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>
Replace the boolean call noise-suppression setting with a 3-way control
(Off / Browser-native / ML beta) in Settings -> General -> Calls.
- Off: noiseSuppression=false to Element Call
- Browser-native: EC's built-in WebRTC suppressor (prior default)
- ML (beta): on-device RNNoise (@sapphi-red/web-noise-suppressor)
Element Call captures the mic inside its iframe and publishes to LiveKit,
so the host can't reach that track; LiveKit's Krisp filter is Cloud-only
(we self-host the SFU) and EC's own RNNoise PR #3892 is unmerged. The ML
tier instead injects a same-origin pre-init shim into the vendored EC
index.html (build/lotus-denoise.js, wired by the lotusDenoise vite plugin)
that patches getUserMedia and routes the captured mic through an RNNoise
AudioWorklet before LiveKit sees it -- the same post-capture pipeline as
#3892, with no EC fork/AGPL/rebase burden. Falls back to the raw mic if
setup fails; keeps echoCancellation/AGC on the raw capture.
- settings.ts: callNoiseSuppression -> 'off'|'browser'|'ml' + legacy
boolean migration (true->browser, false->off)
- CallEmbed/useCallEmbed: tier maps to noiseSuppression param and appends
lotusDenoise=ml (native suppressor off in ML mode)
- vite.config.js: copy RNNoise worklet/wasm + shim into the EC bundle and
inject the shim <script> before EC's module entry
- docs: LOTUS_FEATURES.md, LOTUS_TODO.md (P5-30 done)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New Year: replace flashing animBurst rays with gentle falling confetti
- Lunar New Year: reduce 9 lanterns to 4, halve sizes, dim silk/shimmer
- April Fools: remove all glitch/scanline/watermark effects; replace
with a subtle rainbow stripe and falling punctuation symbols
- Add SeasonalPreview export (position:absolute, reduced-motion) for
use inside contained card elements
- Replace SettingsSelect dropdown for Seasonal Theme with SeasonalBgGrid,
a visual card grid (matches ChatBgGrid pattern) showing ambient previews
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous fix (7f329e3b) made Page transparent so the body background
would show through. But PageRoot sits between Page and body with an opaque
Background.Container color, so the body background was blocked — it only
showed through the glassmorphism sidebar (which is a sibling of PageRoot,
not a child).
Revert to applying getChatBg() directly to Page via inline style, which
overrides PageRoot's class-level background-color by CSS specificity rules.
SidebarNav continues to mirror the same background to document.body so the
glassmorphism sidebar can blur through it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Typing "from:jared" with no additional text now shows all cached
messages from that user across all rooms.
- SearchInput: call onSearch() even when only from: tokens were
extracted (no remaining body text), passing an empty string term
- useLocalMessageSearch: introduce senderOnlyMode (empty term +
senders set) which searches ALL rooms instead of encrypted-only,
and skips text matching — just filters by sender
- MessageSearch: define hasActiveSearch / senderOnlyMode flags;
use them to enable local search and fix placeholder/loading/results
conditions; adapt local results section header and description
Server-side search is skipped in sender-only mode (Matrix search API
requires a search_term); results come from the local event cache.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ExportRoomHistory: make addEvents() async, call decryptEventIfNeeded()
before inspecting type/content so E2EE rooms export decrypted text
- UrlPreviewCard: remove Google S2 favicon (privacy leak); show
generic Icons.Link instead — no third-party external calls
- Profile: add statusDirtyRef so server presence sync cannot clobber
in-flight emoji insertions or keystrokes; cleared on save/clear
- useLocalMessageSearch: include m.sticker, m.poll.start, and
org.matrix.msc3381.poll.start in encrypted room search; index poll
question and answer bodies
- SeasonalEffect: z-index 9997 → 9999 so overlays render above
animated chat backgrounds
- LOTUS_BUGS.md: mark all resolved, document remaining blocked items
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the background was applied directly to <Page> (room view
only) when glassmorphism was off, and to document.body only when
glassmorphism was on. This caused two bugs:
- Without glassmorphism: background only visible in the chat panel,
not behind the sidebar
- With glassmorphism: Page reverted to its opaque theme surface color,
so the body background only showed through the glass sidebar
Fix: SidebarNav now always applies the active background to
document.body (regardless of glassmorphism). RoomView's <Page> is
made transparent whenever a background is active so the body
background shows through both the sidebar and the chat area.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The outer Box(direction=Column, gap=300) was a flex column with flex-shrink:1
on children. With maxHeight:480 + overflowY:auto, when total content exceeded
480px the flex children compressed into each other, making cells appear to
overlap regardless of the gap on the inner flex container.
Replace with:
- Plain div scroll container (display:flex flex-direction:column gap:24)
so children never shrink — they overflow into scroll area
- Plain div per category (gap:10 between label and grid)
- CSS grid (auto-fill, 72px columns, gap:20) for cells so row spacing is
explicit and cannot be affected by flex layout math
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The INSET overflow approach (position:absolute images extending beyond
52×52 buttons) was fundamentally broken: absolutely positioned children
don't contribute to flex row height, so rowGap controlled button-to-button
spacing but image pixels still painted into the gap, causing visual overlap
regardless of how large rowGap was set.
New approach: 72×72 circle cells, overflow:hidden, image fills the cell
via inset:0 with objectFit:contain. Gap of 16px is actual clear space
between cell edges — no math needed. Also bumped maxHeight 420→480.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rowGap 36→52 (40px visual gap between image rows), columnGap separate at 28.
The 52×52 buttons have 8px image overflow on each side so row gap needed to
account for 8+visual+8 = actual gap. Previous 36→20px visual was still tight.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- chatBackground.ts: remove animRainGlowKeyframe and animGridBrightnessKeyframe
from LIGHT anim-rain and anim-pulse definitions (these were removed from the
import and from DARK variants in the previous session but the LIGHT variants
were missed, leaving stale references that would cause a build error)
- ProfileDecoration.tsx: increase decoration grid gap 20→36 (visual gap was
only 4px due to 8px image overflow beyond each 52×52 button), fix paddingBottom
4→8 and add paddingRight:8 to prevent edge clipping
- LOTUS_BUGS.md: correct bug #8 root cause (CSP, not lazy-loading), add
bugs #9 (grid spacing) and #10 (Windows taskbar badge)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings dropdowns (Bug #3):
- Add reusable SettingsSelect component using Menu+PopOut+FocusTrap — exact
same pattern as Message Layout, so all dropdowns look consistent
- Replace raw <select> for Seasonal Theme, UI Font, AFK Timeout, and
Join & Leave Sounds with SettingsSelect
Animated chat backgrounds bleeding onto content (Bug #6 / #7):
- Remove filter:brightness() and opacity animations from chatBackground.ts
(animRainGlowKeyframe, animGridBrightnessKeyframe, animFirefliesGlowKeyframe,
animFirefliesBlinkKeyframe). These were applied to the Page element which
caused ALL descendants (messages, composer) to flash in sync.
Also created a CSS stacking context on Page that pushed SeasonalEffect
(position:fixed; z-index:9997) behind the animated background layer.
- Only backgroundPosition / backgroundSize animations remain — safe, do not
affect descendants, and do not create stacking contexts.
- Remove now-unused animation keyframe imports from chatBackground.ts.
Voice ringing in persistent rooms (Bug #5):
- Narrow the ringing condition from (Invite|Knock|Restricted) to only Invite,
matching exactly the rooms where the call button is visible.
- Add room.isCallRoom() early-exit so m.join_rule:call rooms never ring.
Avatar decoration images not loading (Bug #8):
- Change loading="lazy" → loading="eager" in DecorationPreviewCell.
Lazy loading does not reliably trigger for images inside nested overflow
scroll containers (the settings panel scroll area), so images never loaded.
Docs: LOTUS_BUGS.md updated with root cause and resolution for all 5 new bugs.
Docs: LOTUS_TODO.md adds P5-35/P5-36 (deferred desktop notification/jump list).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Point DECORATION_CDN at Lotus Nextcloud WebDAV share instead of external
avatardecoration.com; all 99 APNG files are self-hosted and served via
direct DAV URL (no CORS issue for <img> elements)
- Add onError handler to AvatarDecoration.tsx to silently hide the overlay
if a file is missing or the CDN is unreachable
- Rewrite scripts/syncDecorations.mjs: now sends HTTP HEAD requests to the
live Nextcloud CDN (batches of 16 in parallel) and removes catalog entries
for files that return non-2xx; empty categories are pruned automatically.
Workflow: delete files from Nextcloud → run `npm run sync:decorations` →
commit the updated avatarDecorations.ts. No local files needed.
- Add public/decorations/ to .gitignore; delete the 85 MB local APNG cache
that was downloaded during development (files live on Nextcloud now)
- Add sync:decorations script to package.json
- Update LOTUS_FEATURES.md, LOTUS_TODO.md (P5-13 + P5-14 ✓), README.md
with avatar decoration documentation and catalog sync workflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
256×256 APNG overlays from avatardecoration.com (open CORS CDN).
Stored in the user's Matrix profile as io.lotus.avatar_decoration via
MSC4133 so all Lotus Chat users see each other's decorations.
- avatarDecorations.ts: curated catalog of 110 original-IP decorations
across 9 categories (Gaming, Cyber, Space, Fantasy, Elements,
Japanese, Nature, Spooky, Cozy)
- useAvatarDecoration: per-user profile fetch with module-level cache
and in-flight deduplication so concurrent renders for the same userId
share one HTTP request
- AvatarDecoration: position:relative wrapper that overlays the APNG
8px beyond the avatar on all sides; renders nothing when no decoration
is set (zero cost for undecorated users)
- ProfileDecoration: scrollable grid picker in Settings → Profile,
grouped by category with live preview; Save button appears only when
the selection differs from what's saved
- Applied at all five avatar display sites: message timeline, members
drawer, knock list, @mention autocomplete, notifications inbox
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds 11 CSS-only seasonal overlays (Halloween, Christmas, New Year, Autumn,
April Fool's, Lunar New Year, Valentine's Day, St. Patrick's Day, Earth Day,
Deep Space, Retro Arcade) with date-based auto-detection and a manual override
dropdown in Settings → Appearance → Seasonal Theme. All themes respect
prefers-reduced-motion. SeasonalEffect mounts at z-index 9997 in App.tsx.
Also rewrites all 5 animated chat background keyframes for smoother, more
organic motion: Digital Rain gains a phosphor glow flicker; Star Drift now
loops each layer by exactly its own tile size (no more seam); Grid Pulse adds
an independent brightness oscillation at a prime period; Aurora Flow drives
all four gradient layers through distinct paths; Fireflies adds glow-pulse and
opacity-blink animations at prime periods for unsynchronised bioluminescence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Room Settings description still said 'Enforced locally by Lotus Chat
clients' from before the voice-limit-guard was deployed. The cap is now
enforced server-side (via the lk-jwt-service guard) for all Matrix clients.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P5-10 Voice Channel User Limit:
- New StateEvent.LotusVoiceLimit (io.lotus.voice_limit) with { max_users }
- RoomVoiceLimit admin control in Room Settings > General > Voice
(power-level gated via permissions.stateEvent)
- CallPrescreen reads the limit reactively and disables Join with a
'Channel Full (N/N)' message at capacity; existing members can rejoin
P5-16 Custom Join/Leave Sound Effects:
- useCallJoinLeaveSounds hook wired into CallUtils; detects participant
join/leave via MatrixRTCSession membership changes (sender|deviceId),
filters out self, only fires while joined
- Sounds synthesized in-browser with Web Audio (callSounds.ts) - no
assets bundled; styles Off/Chime/Soft/Retro
- 'Join & Leave Sounds' selector in Settings > Calls (previews on change)
Docs: LOTUS_FEATURES.md, README.md, LOTUS_TODO.md (P5-10/P5-16 marked done)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P4-3 — Knock-to-join Notifications for Admins:
- usePendingKnocks() hook reactively tracks knocking members via
RoomMemberEvent.Membership; returns empty array if user lacks invite power
- Members icon in RoomViewHeader shows a Warning badge with the knock count
when there are pending requests; badge updates in real time without
needing to open the drawer; aria-label updated to describe pending count
P5-11 — AFK Auto-Mute in Voice:
- useAfkAutoMute() hook opens a monitoring-only getUserMedia stream,
connects it to an AnalyserNode, and polls RMS every 500ms
- If mic is on and RMS stays below threshold for the configured timeout,
calls callEmbed.control.setMicrophone(false) and shows an in-app toast
- Hook is called inside CallControls so monitoring is only active during calls
- Settings: afkAutoMute toggle + afkTimeoutMinutes select (1/5/10/20/30 min,
default 10) added to Settings → Calls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- EditHistoryModal: decrypt fetched edit events in E2EE rooms via
mx.decryptEventIfNeeded() before rendering; previously events not
found in the room cache showed ciphertext or "(no text)"
- CallEmbedProvider: add touch support for PiP resize corners;
extracted shared applyResize() helper; onTouchStart wired to all
four corners alongside existing onMouseDown
- RoomView: skip chatBgStyle when glassmorphism is active; document.body
already carries the background for the blur effect, rendering it twice
doubled CSS animation work unnecessarily
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>
8 individual SettingTile rows collapsed into a single wrapped chip grid.
Active chips show as Primary+outlined; inactive as Secondary. Clicking
any chip toggles it. Drops from ~83 lines to ~25 and reads at a glance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- P5-21: Custom @mention highlight color picker in Settings → Appearance.
CSS vars with luminance-computed text color; resets cleanly to theme default.
- P5-22: Font selector (System, Inter, JetBrains Mono, Fira Code) in
Settings → Appearance. Fira Code added to Google Fonts preload.
- P5-27: Gaming/Work/Sleep preset buttons at top of Settings → Notifications.
Each atomically applies a group of notification settings.
- AppearanceEffects component in App.tsx applies CSS vars on settings change.
- LOTUS_BUGS.md: mark presence + manifest icon bugs as resolved.
- LOTUS_TODO.md: mark P3-6, P5-21, P5-22, P5-27 as [x].
- LOTUS_FEATURES.md: document all four completed features.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each button (Format, Emoji, Sticker, GIF, Location, Poll, Voice,
Schedule) can be individually hidden in Settings → Editor.
All default to on, stored in composerToolbarButtons object.
getSettings deep-merges the nested object so new buttons default
to true for existing users with saved settings.
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>