Element Call is now consumed as our self-built fork
(@lotusguild/element-call-embedded); wire up its previously-dormant
capabilities and document the fork as live.
Soundboard (P5-15): a call-bar button plays user-uploaded audio clips into the
call as a real published track (io.lotus.inject_audio) plus local playback.
Clips are uploadable like emoji/sticker packs, stored in io.lotus.soundboard
account data (synced across devices). Gated by a Settings toggle + volume.
Quality controls (P5-31): per-user mic/screenshare bitrate + screenshare
framerate (Settings -> Calls), applied via io.lotus.set_quality clamped to any
room cap. Room admins set caps and hard call-permissions (allow_screenshare /
allow_camera) in Room Settings -> Voice; the call bar hides blocked buttons.
- New: CallSoundboard, useSoundboard, soundboardClips; RoomQuality,
useCallQuality, callQuality (+ unit tests).
- Optimistic-write RoomQuality admin UI (no stale-state clobber).
- Docs: mark EC fork live across README/FEATURES/TODO/BUGS/TESTING; add D2
manual-test steps.
Numeric quality caps are client-cooperative; screenshare/camera permissions are
hard-enforced server-side (see LotusGuild/matrix voice-limit-guard).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the 808-line SeasonalEffect monolith into one self-contained module per
theme under seasonal/themes/ (<Theme>.tsx + <Theme>.css.ts), and gave every
theme a premium, research-backed redesign (one Opus agent per theme against a
shared brief). SeasonalEffect now just imports the 11 overlays and dispatches;
the orphaned shared Seasonal.css.ts is removed (each theme owns its keyframes).
Each overlay: layered oklch palettes, GPU-only animation (transform/opacity),
`contain: layout paint style` to kill repaint flicker, ≤~40-element perf budget,
particles seeded once via useMemo (no per-frame state), a gorgeous STATIC
prefers-reduced-motion form (the settings preview thumbnail), WCAG-AA-preserving
low opacities, and no new deps / no external assets (inline SVG data-URIs,
Tauri/CSP-safe).
Themes: Halloween, Christmas, New Year, Autumn, April Fools, Lunar New Year,
Valentines, St. Patrick's, Earth Day, Deep Space, Arcade.
Gates: tsc clean, ESLint clean, Prettier clean, build OK, 551 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings never told the user which days "Auto" turns each seasonal theme on.
Extracted the date windows out of getActiveSeason into a shared SEASON_SCHEDULE
(seasonSchedule.ts) — the single source of truth for both the runtime Auto
selector and the settings UI, so displayed dates can't drift from real activation.
- seasonal/types.ts: SeasonTheme + SeasonalOverlayProps (leaf module).
- seasonal/seasonSchedule.ts: priority-ordered SEASON_SCHEDULE with human date
ranges + SEASON_DATE_RANGES + getActiveSeason (behavior-preserving refactor).
- SeasonalEffect.tsx: consume the shared type/selector; re-export SeasonTheme.
- General.tsx: per-theme date caption under each swatch ("Oct 15 – Nov 1"), Auto
reads "By calendar", and the section description explains it.
- seasonSchedule.test.ts (6): representative day per theme, overlap priority
(Deep Space > Autumn, New Year > Lunar), inclusive boundaries, off-season null.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three issues from live testing:
- A1: the 'classic' ringtone (call.ogg, mastered near full scale) was much
louder than the synthesized styles. Attenuate it (CLASSIC_GAIN 0.45) so all
ringtones sit at a comparable level.
- A3/A4: the caller had no indication when a DM/group callee declined — their
UI kept "ringing" until the notification lifetime expired. IncomingCallListener
now listens for RTCDecline events for a call we're hosting in the room and
toasts the caller ("<name> declined your call").
- G1: the PiP "All muted" badge fired when any single remote participant muted.
useRemoteAllMuted now returns true only when there is >=1 remote and every
remote participant is muted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Centralized the global floating-UI stacking values into styles/zIndex.ts
(inCallBanner 9990 < seasonalEffect 9997 < nightLight 9998 < toast 10001;
folds modals sit at 9999 between). Same values, no behavior change — just
removes the magic numbers and documents the layering so future overlays don't
collide. Component-internal small z-index stays local.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaced the last raw native <select> (Profile timezone, colorScheme:'dark')
with SettingsSelect. Added an optional `disabled` prop to SettingsSelect for
the saving state. handleSubmit reads the `timezone` state (not the native form
field) so submission is unaffected; the now-unused handleSelectChange was
removed. No raw <select> elements remain in the settings UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extracted the folds-native dropdown (Button+PopOut+Menu) from General.tsx into a
shared components/settings-select/SettingsSelect.tsx, and used it to replace raw
native <select> elements (which render OS-styled and broke under non-default
themes via colorScheme:'dark'):
- Profile "auto-clear after" select
- PushRuleEditor add-rule mode select (dropped the now-unused handleModeChange)
The form-tied timezone <select> in Profile is left for a follow-up (it's wired
to native form submission + a disabled state and needs more care).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- RouteError: raw <div>/<h2>/<p>/<button> (sans-serif, raw px) -> folds
Box/Text/Button with config tokens.
- CallEmbedProvider PiP fullscreen control: raw <button> with ⊡/⛶ glyphs ->
folds IconButton reusing the exported FullscreenIcon/ExitFullscreenIcon SVGs
from Controls (consistent with the main fullscreen button). The intentional
dark over-video scrim is kept.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- N122: setMediaState resolves on EC's transport ACK instead of waiting for a
DeviceMute state-echo that EC may elide or skip during teardown — which
previously stranded the promise forever and silently skipped the initial
deafen state + first StateUpdate on join. Dropped the single-slot
mediaStatePromiseResolver; onMediaState remains the authoritative sync path.
- N123: focusCameraParticipant now waits for a spotlight videoTile to mount via
a MutationObserver (with a 600ms hard-timeout fallback) instead of a fixed
2-frame delay that EC's React commit can exceed on slower devices.
- N126: PiP position restored from localStorage is shape+finiteness validated,
so corrupt data can't feed NaN into the position math (invalid 'NaNpx' CSS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review flagged that wrapping only the buttons left the dialog body copy
hardcoded (mixed-language dialogs once a non-en locale ships). Wrap the
remaining body/waiting strings ("Please accept…", "Confirm the emoji…",
"Do not Match", "Your device is verified.", etc.) and the PasswordStage
prompt, adding hooks to the sub-components that lacked one. Keys added to
en.json; all t() keys verified to resolve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CallEmbed watchdog now SELF-HEALS: a genuine ready/joined signal arriving
after the 25s timeout clears the error and notifies subscribers with
undefined, so a slow-but-successful EC load no longer strands the user on
the recovery screen over a live call. Listener dispatch wrapped in try/catch.
- ringtones: synth notes route through a per-session master gain; stop() ramps
it to 0 so the ring is silenced instantly on answer instead of letting the
last scheduled phrase ring out over call audio.
- IncomingCallBanner: ping fires exactly once per incoming call (guarded by
refEventId) instead of re-pinging when ringtone settings change mid-banner.
- focusCameraParticipant: try multiple tile selectors (EC labels vary by
version), defer the tile click past EC's async spotlight layout switch
(rAF x2), and dev-warn when no tile matches so testers get signal.
- uploadContent: a cancelled upload (mx.cancelUpload -> AbortError) is no
longer treated as retryable — previously the retry loop could resurrect an
upload the user just cancelled. Also retry on 408.
- addRoomIdToMDirect/removeRoomIdFromMDirect: guard against a corrupt m.direct
whose values aren't arrays.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wraps the hardcoded strings flagged in LOTUS_BUGS.md (Localization rows)
in t() via react-i18next, and adds the keys to public/locales/en.json
under the existing Organisms.* namespace. de.json intentionally left to
fall back to en for now (fallbackLng: 'en') rather than fabricate
translations.
Files: CreateRoomTypeSelector, ImageViewer, MsgTypeRenderers (MLocation),
Reply (ThreadIndicator), ImageContent, DeviceVerification (5 subcomponents),
UrlPreviewCard (DiscordCard), InviteUserPrompt, UploadBoard, PasswordStage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Poll answer buttons referenced undefined CSS vars (--accent-cyan,
--accent-cyan-dim, --accent-cyan-border, --border-color) plus hardcoded
rgba()/#fff and raw rem font sizes, so they rendered unstyled on every
non-TDS theme (invisible borders, no selected/progress state).
Replace all colors with always-defined folds tokens (Primary.* for the
selected/indicator state, SurfaceVariant.* for the resting surface +
progress fill), size/spacing/radii with config.* tokens, and the
checkbox/radio glyphs + percentage/label text with folds <Text>. The
progress-bar-behind-text affordance is preserved (folds Button has no
equivalent), now theme-reactive. Merged the duplicate checkbox/radio
indicator spans into one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously a second incoming call was dropped from the UI entirely when the
user was already in a call (`!joined && callInfo`). Now, when joined to a
different call, a compact corner banner (caller avatar + name + Answer/Reject)
is shown instead of the full-screen IncomingCall overlay, with a single soft
ping (one-shot ringtone) rather than the looping ring so it doesn't talk over
the active call. The full overlay still shows when not in any call; being in
the ringing room's own call still shows nothing.
Built with folds primitives + TDS tokens (no hardcoded colors).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a ringtoneId setting (classic | chime | soft | retro | none) so the
incoming-call ring is no longer hardcoded to call.ogg. The three synth
styles are generated in-browser via a new utils/ringtones.ts module
(mirroring the existing callSounds.ts WebAudio pattern), so no new binary
assets are bundled; 'classic' keeps the existing call.ogg clip and 'none'
is a silent, visual-only incoming-call UI.
- ringtones.ts: startRingtone() loops until stopped; previewRingtone()
plays a single non-looping preview and auto-cancels the prior preview.
- IncomingCall: ring driven by the setting; <audio> element removed.
- Settings > Calls: Ringtone selector with on-select preview, beside the
existing Ringtone Volume slider.
- settings.ts: persisted value whitelisted back to a known id.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- RoomTimeline: wrap jump-to-latest/unread + mark-as-read handlers in useCallback
(the handlers passed to memoized message children were already memoized).
- RoomInput: wrap file/upload/emoji/sticker/location callbacks in useCallback so
the editor and toolbar don't re-render needlessly.
- EmojiBoard: hoist repeated mx.getRoom() pack-label lookups into a useMemo'd map
in the emoji and sticker sidebars (previously called per-render in map loops).
Behavior unchanged. (RoomTimeline/RoomInput already have ErrorBoundary wrappers
in RoomView, so no boundary added.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- N69: @mention highlight color now uses HexColorPickerPopOut + react-colorful
HexColorPicker behind a folds Button (color swatch); built-in onRemove
replaces the separate Reset, dropping the OS-native <input type="color">
- N10: mentionPulseKeyframes animates only box-shadow (dropped the imperceptible
scale(1.003)) so it no longer fights MsgAppearClass over `transform` on
self-sent @mention messages
- N22: Direct.tsx virtualizer estimateSize 38 -> 52 (two-line DM row height) to
avoid the initial-render jump before measureElement corrects each row
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pure formatting reflows (multi-line wrapping of long lines/imports/tables);
no behavior change. Clears the working tree of pending prettier diffs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four changes to match screenshare full-screen UX for camera feeds:
1. Fullscreen button always visible
CallControls.tsx: remove `screenshare &&` gate — the ⛶ fullscreen
button now appears in camera-only calls, not just during screenshare.
2. Per-participant camera focus (CallControl.focusCameraParticipant)
Finds the target's video tile in the EC iframe DOM via:
[data-testid="videoTile"] / [data-video-fit]
closest ancestor of [aria-label="${userId}"]
Enables spotlight mode if not already active, then clicks the tile
so EC's internal focus handler runs. Falls back gracefully if the
tile is not in the DOM (camera off).
3. MemberGlance participant popup
Clicking a participant avatar in the call status bar now shows a
small menu: "Focus camera" (calls focusCameraParticipant) and
"View profile" (existing behaviour). Previously it opened the
profile immediately with no way to focus the camera.
4. PiP fullscreen button
A ⛶/⊡ icon button appears in the PiP overlay top-right area,
letting users go fullscreen directly from PiP mode without
navigating back to the call room first.
UNTESTED — requires a real multi-participant call to verify tile
clicking behaviour and fullscreen transitions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously PipMuteOverlay fired on useRemoteAllMuted (any remote
muted) and rendered in the bottom-left corner — the conventional
position for local-user mic status — causing users to think their
own mic was muted when it wasn't.
Fix: split into two distinct indicators
- Bottom-left: local mic muted only (from useCallControlState),
labelled "You" so attribution is unambiguous
- Top-right: "All muted" warning (warning color, not critical) when
all remote participants are muted
UNTESTED — verify in a real call at chat.lotusguild.org.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap RoomTimeline in ErrorBoundary — a single bad event no longer
crashes the entire timeline; shows a graceful "Timeline unavailable"
message instead
- Wrap RoomInput in ErrorBoundary — composer crashes show a fallback
placeholder rather than a blank white section
- Animate SpeakerAvatarOutline with a 1.2s pulse keyframe so it's
visually distinct from a static ring; respects prefers-reduced-motion
- Fix var(--border-surface-variant) undefined variable in UserRoomProfile
device session rows; replaced with color.SurfaceVariant.ContainerLine
UNTESTED — verify at chat.lotusguild.org post-deploy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds 'Ringtone Volume' slider (0–100, default 70%) to Settings → Calls.
The IncomingCall audio element reads the setting and applies it as
audioElement.volume before playing, replacing the implicit browser
default of 1.0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reaction.tsx now computes aria-label='{shortcode} reaction, N people'
using getShortcodeFor so screen readers announce emoji name and count
instead of an ambiguous button. Custom (mxc://) emoji falls back to
'custom emoji reaction'.
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>
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>
Creates useNearViewport hook (IntersectionObserver, 200px rootMargin,
one-shot disconnect after first trigger). ImageContent and VideoContent
now gate loadSrc() on nearViewport — when autoPlay is enabled, encrypted
media is not decrypted until the element is within 200px of the visible
area, reducing initial page load cost on long timelines.
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>
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>
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>
* fix: update PageHero styles for better text handling and add PageHeroText class
* trigger cla recheck
* refactor: remove unused styles from PageHeroText and LobbyHeroTopic
* fix: remove unnecessary class names from PageHero for cleaner markup
style: add styles to LobbyHeroTopic for improved text display
- 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>
- UserHero: move AvatarDecoration inside AvatarPresence so the decoration
inline-flex container sizes to the avatar only, not the presence badge
- SidebarNav: add will-change: background-position, background-size on
document.body for animated backgrounds, promoting them to a compositor
layer so overlaid text/UI doesn't repaint on every animation frame
- scheduledMessages: back the atom with atomWithStorage so the scheduled
message tray survives page refreshes via localStorage
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>
AvatarPresence had position:absolute via UserAvatarContainer, which
made it position relative to the AvatarDecoration wrapper (a new
stacking context) instead of the hero container, shifting the
decoration image one avatar-width to the left.
Fix: apply UserAvatarContainer to an outer div so AvatarDecoration
sits inside the absolute-positioned slot, not the other way around.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Selected option borders and backgrounds used rgba(var(--mx-primary-rgb,
0,132,255), ...) which rendered as the default Cinny blue, ignoring the
Lotus Terminal Design System palette. Replaced with --accent-cyan,
--accent-cyan-dim, --accent-cyan-border, and --border-color.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>