Compare commits
2 Commits
10f6544e2e
...
f9edd2023d
| Author | SHA1 | Date | |
|---|---|---|---|
| f9edd2023d | |||
| 30101c83e8 |
+53
-42
@@ -5,57 +5,68 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
|
||||
|
||||
---
|
||||
|
||||
## ✅ Resolved Issues
|
||||
## 🚩 Critical & UI Bugs
|
||||
|
||||
- **Ringing Modal Fires in Voice Rooms**: Fixed in `CallEmbedProvider.tsx` — only `notification_type === 'ring'` events now trigger the modal.
|
||||
- **Avatar Decoration Displacement in Profile**: Fixed in `UserHero.tsx` — `UserAvatarContainer` (position:absolute) now wraps `AvatarDecoration` as the outermost element, keeping the positioning context relative to `UserHeroAvatarContainer`.
|
||||
- **Export History Broken for E2EE**: Fixed in `ExportRoomHistory.tsx` — `addEvents` is now async and calls `mx.decryptEventIfNeeded()` before inspecting event type/content.
|
||||
- **Privacy Leak in URL Previews (Google Favicon)**: Fixed in `UrlPreviewCard.tsx` — Google S2 favicon call removed; a generic folds `Icons.Link` icon is shown instead.
|
||||
- **Status Emoji Picker Doesn't Insert Emoji**: Fixed in `Profile.tsx` — `statusDirtyRef` prevents the server-presence sync `useEffect` from overwriting in-flight user input (cleared on save/clear).
|
||||
- **Encrypted Search Misses Stickers and Polls**: Fixed in `useLocalMessageSearch.ts` — `m.sticker`, `m.poll.start`, and `org.matrix.msc3381.poll.start` events now included; poll question and answer bodies are indexed.
|
||||
- **Seasonal Themes Display Behind Chat Background**: Fixed in `SeasonalEffect.tsx` — z-index bumped from 9997 to 9999.
|
||||
- **Windows Taskbar Badge Black Square**: Fixed in `src-tauri/src/lib.rs` — `std::ptr::write_bytes` zeros the DIB bits buffer immediately after `CreateDIBSection`; previously uninitialized bytes caused garbage pixels with alpha=255.
|
||||
### 1. Avatar Decoration Displacement in Profile
|
||||
**File:** `src/app/components/user-profile/UserHero.tsx`
|
||||
**Status:** **OPEN**
|
||||
|
||||
---
|
||||
* **Issue:** Avatar decorations appear displaced left of the avatar when viewing the profile modal.
|
||||
* **Root Cause:** The `AvatarPresence` badge sticking out to the right shifts the center of the `inline-flex` container. The decoration centers on the container, not the avatar.
|
||||
* **Recommended Fix:** Wrap only the `Avatar` component with `AvatarDecoration`.
|
||||
|
||||
## 🛡️ Pending — Critical Security & Logic
|
||||
|
||||
*(none currently open)*
|
||||
|
||||
---
|
||||
|
||||
## 🚩 Pending — Functional & UI Bugs
|
||||
|
||||
### 1. Avatar Decoration Images Not Rendering
|
||||
**File:** `src/app/features/lotus/avatarDecorations.ts`
|
||||
**Status:** **OPEN — Blocked on infrastructure**
|
||||
|
||||
* **Issue:** Decoration images in Settings do not load.
|
||||
* **Root Cause:** The Nextcloud WebDAV URL (`public.php/dav/files/…`) returns `Content-Disposition: attachment`, which browsers refuse to render in `<img>` tags.
|
||||
* **Required Fix:** Move decoration assets to a CDN or file host that returns `Content-Type: image/png` with `Content-Disposition: inline`. Options: Gitea LFS, Cloudflare R2, or a dedicated static file host. Alternatively, reconfigure the Nextcloud share to serve inline.
|
||||
* **Blocked by:** Infrastructure change (not a code fix).
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Pending — UI/UX & Visual Consistency
|
||||
|
||||
### 1. Inconsistent Settings Dropdown Styling
|
||||
### 2. Inconsistent Settings Dropdown Styling
|
||||
**Files:** `Profile.tsx`, `SystemNotification.tsx`
|
||||
**Status:** **OPEN — Low priority**
|
||||
**Status:** **OPEN**
|
||||
|
||||
* **Issue:** Dropdowns for Status Expiry and Notification Sounds use raw HTML `<select>` elements.
|
||||
* **Recommended Fix:** Replace with the custom-styled `Menu` + `PopOut` pattern used in `General.tsx`.
|
||||
|
||||
### 2. Animations Flickering Content (Fireflies)
|
||||
**File:** `src/app/features/lotus/chatBackground.ts`
|
||||
**Status:** **OPEN — Low priority**
|
||||
### 3. Ringing Modal Fires in Voice Rooms
|
||||
**File:** `src/app/components/CallEmbedProvider.tsx`
|
||||
**Status:** **OPEN**
|
||||
|
||||
* **Issue:** Complex radial-gradient animations cause flickering on some GPUs.
|
||||
* **Recommended Fix:** Scope animations strictly to background properties and simplify heavy gradients.
|
||||
* **Issue:** Joining a static voice room triggers the "Incoming Call" ringing.
|
||||
* **Recommended Fix:** Check `notification_type` in the Matrix RTC event. Only 'ring' should trigger the modal.
|
||||
|
||||
### 3. No Camera Focus During Screenshare
|
||||
### 4. No Camera Focus During Screenshare
|
||||
**File:** `src/app/features/call/CallControls.tsx`
|
||||
**Status:** **OPEN — Blocked on Element Call internals**
|
||||
**Status:** **OPEN**
|
||||
|
||||
* **Issue:** There is no way to focus a camera while screenshare is active.
|
||||
* **Recommended Fix:** Implement a "Pin/Focus" toggle on participant tiles — requires Element Call iframe API support.
|
||||
* **Issue:** When someone is screensharing and another participant turns on their camera, there is no way to switch the primary display to the camera or go fullscreen on it.
|
||||
* **Recommended Fix:** Implement a "Focus" toggle on participant tiles that overrides the automatic screenshare spotlight.
|
||||
|
||||
### 5. Chat Background Animation Flickering
|
||||
**File:** `src/app/features/lotus/chatBackground.ts`
|
||||
**Status:** **OPEN**
|
||||
|
||||
* **Issue:** Some animated backgrounds (like Fireflies) cause flickering/flashing of the message text and composer area on certain browsers/GPUs.
|
||||
* **Recommended Fix:** Ensure animations are scoped strictly to background properties (`background-position`, `background-size`) and do not use properties like `filter` or `opacity` on the main container.
|
||||
|
||||
---
|
||||
|
||||
## 📱 PWA & Mobile Issues
|
||||
|
||||
### 1. Exclusive Background vs. Seasonal Choice
|
||||
**Status:** **OPEN**
|
||||
|
||||
* **Issue:** Users can have both a Chat Background and a Seasonal Theme active, causing visual clutter and excessive GPU usage on mobile.
|
||||
* **Recommended Fix:** Implement a "Choose One" toggle in Settings.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Technical & Performance Refinements
|
||||
|
||||
### 1. Decrypted Media Memory Leak (Gallery & Lightbox)
|
||||
**File:** `src/app/features/room/MediaGallery.tsx`
|
||||
**Status:** **OPEN**
|
||||
|
||||
* **Issue:** Every image in the gallery history is decrypted and converted to a Blob URL simultaneously.
|
||||
* **Recommended Fix:** Implement virtualization for the gallery grid.
|
||||
|
||||
### 2. Scheduled Messages are Ephemeral
|
||||
**File:** `src/app/state/scheduledMessages.ts`
|
||||
**Status:** **OPEN**
|
||||
|
||||
* **Issue:** Refreshing the page clears the "Scheduled" tray, making it impossible for users to see or cancel messages they have already scheduled.
|
||||
* **Recommended Fix:** Persist the scheduled message metadata in `localStorage`.
|
||||
|
||||
+9
-102
@@ -121,14 +121,6 @@ Status: `[ ]` pending · `[~]` in progress · `[x]` completed
|
||||
|
||||
---
|
||||
|
||||
### [x] P3-6 · Configurable Composer Toolbar
|
||||
|
||||
**What:** Let users rearrange or hide individual composer toolbar buttons (GIF, Sticker, Emoji, File, Voice, Location). Changes stored in `settingsAtom`. Access via a small "⚙ Customize toolbar" option in toolbar overflow.
|
||||
**[AUDIT REQUIRED]** — Audit the current toolbar button rendering in `RoomInput.tsx`. Understand the layout system (is it a fixed array or already mapped from config?). Drag-to-reorder may require a DnD library; consider whether reorder is worth the complexity vs just toggle-visibility.
|
||||
**Complexity:** Medium-High (drag reorder adds significant complexity).
|
||||
|
||||
---
|
||||
|
||||
### [ ] P3-8 · Thread Panel (full side drawer)
|
||||
|
||||
**⚠️ LARGEST FEATURE — requires its own planning session before implementation.**
|
||||
@@ -172,14 +164,6 @@ Features:
|
||||
|
||||
---
|
||||
|
||||
### [x] P4-3 · Knock-to-join Notifications for Admins
|
||||
|
||||
**Note:** The basic knock-to-join UX is covered in P1-11 (completed). This task adds the admin notification side.
|
||||
**What:** Space/room admins see a notification badge when there are pending knock requests. A "Pending Join Requests" section in the members drawer or room settings. Approve (invite) or deny (kick) each knock.
|
||||
**Complexity:** Medium.
|
||||
|
||||
---
|
||||
|
||||
### [ ] P4-4 · Math / LaTeX Rendering in Messages (LOW PRIORITY)
|
||||
|
||||
**Spec:** CS-API §11.5 (stable) — `formatted_body` can contain LaTeX.
|
||||
@@ -243,53 +227,6 @@ Themes:
|
||||
|
||||
---
|
||||
|
||||
### [x] P5-10 · Voice Channel User Limit
|
||||
|
||||
**What:** Admins set max participants via custom state event `io.lotus.voice_limit: { max_users: N }`. Show "Channel Full (5/5)" to users over the limit. Local enforcement only.
|
||||
**[AUDIT REQUIRED]** Check if Element Call has its own participant limit that should be integrated with rather than duplicated.
|
||||
**Complexity:** Medium.
|
||||
**Done:** `RoomVoiceLimit` admin control in Room Settings → General → Voice; `CallPrescreen` disables Join + shows "Channel Full (N/N)" when at capacity (rejoiners exempt). State event `StateEvent.LotusVoiceLimit`. **Hard enforcement is server-side for ALL clients** via `voice-limit-guard` (matrix repo `livekit/voice-limit-guard.py`) — a fail-open sidecar fronting `lk-jwt-service` (guard `:8070`, lk-jwt `:8071`) that refuses the LiveKit JWT (403) when the room is at capacity. The client check is UX-only. EC has only a global `max_participants` (50), so per-room limits were not duplicating an EC feature.
|
||||
|
||||
---
|
||||
|
||||
### [x] P5-11 · AFK / Idle Auto-Mute in Voice
|
||||
|
||||
**What:** Auto-mute mic after X minutes of silence (detected via Web Audio AnalyserNode). Show "You were auto-muted due to inactivity" toast with click-to-unmute. Admin-configurable via `io.lotus.afk_timeout` state event. Disableable in Settings → Calls.
|
||||
**[AUDIT REQUIRED]** Verify auto-mute must go through the same CallControl bridge as manual mute.
|
||||
**Complexity:** Medium.
|
||||
|
||||
---
|
||||
|
||||
### [x] P5-12 · Seasonal / Event Themes
|
||||
|
||||
**What:** Automatic + manually toggleable seasonal overlays with CSS particle effects and accent color variants:
|
||||
|
||||
- **Halloween** (Oct 15–Nov 1): purple particles, orange accents, spider web pattern
|
||||
- **Christmas** (Dec 10–Jan 2): snow fall, red/green accents, snowflake pattern
|
||||
- **New Year** (Dec 31–Jan 1): firework burst animation, gold accents
|
||||
- **Pride** (June): rainbow gradient accent cycle
|
||||
All toggleable manually in Settings → Appearance regardless of date. Respects `prefers-reduced-motion`.
|
||||
**[AUDIT REQUIRED]** Design against existing CSS animation system in `lotus-terminal.css.ts`.
|
||||
**Complexity:** Medium.
|
||||
|
||||
---
|
||||
|
||||
### [x] P5-13 · Avatar Frame / Border Decorations
|
||||
|
||||
**What:** Decorative CSS rings/frames rendered around user avatars. Built-in options: TDS Glow (animated orange pulsing), Cyberpunk (rotating gradient), Minimal (thin ring), Gold (supporter cosmetic). Stored in Matrix account data `io.lotus.avatar_frame`. Only visible in Lotus Chat.
|
||||
**[AUDIT REQUIRED]** Verify folds Avatar component allows overlay decoration without breaking child-type constraints (see previous white-circle avatar bug).
|
||||
**Complexity:** Medium.
|
||||
|
||||
---
|
||||
|
||||
### [x] P5-14 · Animated Avatar Overlay Decorations (Discord-style)
|
||||
|
||||
**What:** Animated WebM/GIF overlays that float around avatars (transparent center showing avatar). Curated built-in set OR user-uploaded mxc:// overlay. Stored in account data. Only Lotus Chat users see them.
|
||||
**[AUDIT REQUIRED]** See #P5-13 audit. Also decide: curated set only vs user-uploadable.
|
||||
**Complexity:** Medium.
|
||||
|
||||
---
|
||||
|
||||
### [ ] P5-15 · In-Call Soundboard
|
||||
|
||||
**What:** Grid of short audio clips playable into the call audio stream via Web Audio API (AudioBufferSourceNode → MediaStreamDestinationNode → mixed with mic). Built-in clips + user-uploadable custom clips (stored as mxc://). Accessible from call controls bar.
|
||||
@@ -298,15 +235,6 @@ Themes:
|
||||
|
||||
---
|
||||
|
||||
### [x] P5-16 · Custom Join / Leave Sound Effects
|
||||
|
||||
**What:** Local-only sounds when participants join/leave a call you're in. Built-in options + per-user settable. Detect via Element Call participant list change events.
|
||||
**[AUDIT REQUIRED]** Find how Element Call exposes join/leave participant events to the parent window via postMessage bridge.
|
||||
**Complexity:** Medium.
|
||||
**Done:** Detected via `MatrixRTCSession` membership changes (`useCallMembersChange`) rather than the EC postMessage bridge — more reliable, identity tracked by `sender|deviceId`. Sounds synthesized with Web Audio (no assets). Styles Off/Chime/Soft/Retro in Settings → Calls. Hook `useCallJoinLeaveSounds`, util `callSounds.ts`.
|
||||
|
||||
---
|
||||
|
||||
### [ ] P5-20 · Quick Reply from Browser Notification
|
||||
|
||||
**What:** Inline reply field in browser notification toasts via Notification Actions API. Reply sends as threaded reply to the triggering message.
|
||||
@@ -315,28 +243,6 @@ Themes:
|
||||
|
||||
---
|
||||
|
||||
### [x] P5-21 · Custom @Mention Highlight Color
|
||||
|
||||
**What:** Each user sets their own mention highlight color in Settings → Appearance. Applied as `--user-mention-color` CSS property override on mention-highlighted message rows.
|
||||
**Complexity:** Low.
|
||||
|
||||
---
|
||||
|
||||
### [x] P5-22 · Font Selector for the UI
|
||||
|
||||
**What:** Font picker in Settings → Appearance. Options: JetBrains Mono, Inter, Geist, Fira Code, OpenDyslexic, System Default. Applied via CSS custom property overrides.
|
||||
**[AUDIT REQUIRED]** Check if any fonts are already globally loaded to avoid double-loading.
|
||||
**Complexity:** Low-Medium.
|
||||
|
||||
---
|
||||
|
||||
### [x] P5-27 · Notification Profile Presets (Gaming / Work / Sleep)
|
||||
|
||||
**What:** Saved presets that change all notification settings atomically. Gaming (mentions only), Work (DMs + mentions), Sleep (all off). Quick-switch from sidebar or settings.
|
||||
**Complexity:** Medium.
|
||||
|
||||
---
|
||||
|
||||
### [ ] P5-30 · Advanced ML Noise Suppression (Krisp-style)
|
||||
|
||||
**What:** High-end background noise cancellation using a pre-trained ML model (e.g. RNNoise) running in the browser. Removes dogs, fans, and keyboard clicks from the mic stream.
|
||||
@@ -355,14 +261,6 @@ Themes:
|
||||
|
||||
---
|
||||
|
||||
### [x] P5-34 · User-to-User Private Notes
|
||||
|
||||
**What:** A private "Notes" field on user profiles visible only to you. Syncs across all your devices.
|
||||
**Matrix Tech:** Store in `io.lotus.user_notes` account data. Must be keyed by `userId`.
|
||||
**Complexity:** Medium.
|
||||
|
||||
---
|
||||
|
||||
### [ ] P5-35 · Desktop — Notification Click Opens Room (DEFERRED)
|
||||
|
||||
**What:** Clicking a system tray notification navigates to the relevant room. Quick-reply from the notification toast would send the reply without opening the window.
|
||||
@@ -381,6 +279,15 @@ Themes:
|
||||
|
||||
---
|
||||
|
||||
### [ ] P5-40 · Desktop — Proactive Update Notifications (Tauri)
|
||||
|
||||
**What:** Automatically check for app updates on launch and periodically during long sessions. If an update is available, show an in-app toast or badge (e.g. on the Settings icon) to alert the user without requiring them to manual check in settings.
|
||||
**Mechanism:** Use the `useTauriUpdater` hook in a global component like `ClientNonUIFeatures.tsx`.
|
||||
**Note:** Ensure the check is throttled (e.g. once every 12 hours) to avoid redundant Tauri commands.
|
||||
**Complexity:** Low-Medium.
|
||||
|
||||
---
|
||||
|
||||
## Blocked Features
|
||||
|
||||
These features are confirmed desirable but cannot be built until the listed dependency is resolved.
|
||||
|
||||
+31
-68
@@ -32,46 +32,6 @@ This document provides exhaustive, low-level implementation details for the rema
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Priority 5 — Gamer / Aesthetic / Customization
|
||||
|
||||
### P5-1 · Custom Accent Color Picker (Non-TDS only)
|
||||
**Mechanism:** Dynamic CSS variable injection.
|
||||
|
||||
* **1. Setting (src/app/state/settings.ts):**
|
||||
Add `customAccentColor: string` (hex).
|
||||
* **2. Manager (src/app/pages/ThemeManager.tsx):**
|
||||
Inside the `useEffect` that monitors theme changes:
|
||||
```typescript
|
||||
if (!lotusTerminal && customAccentColor) {
|
||||
document.documentElement.style.setProperty('--lt-accent-orange', customAccentColor);
|
||||
// Also derive a 'glow' version (e.g. 50% opacity)
|
||||
document.documentElement.style.setProperty('--lt-accent-orange-glow', `${customAccentColor}80`);
|
||||
}
|
||||
```
|
||||
* **3. UI (src/app/features/settings/general/General.tsx):**
|
||||
Use a `<Input type="color">` component. Hide this section if `lotusTerminal` is `true`.
|
||||
|
||||
### P5-14 · Animated Avatar Overlay
|
||||
**Mechanism:** CSS Pseudo-element wrapping.
|
||||
|
||||
* **1. Wrapper (src/app/components/user-avatar/UserAvatar.tsx):**
|
||||
```tsx
|
||||
const avatar = (
|
||||
<AvatarImage className={classNames(css.UserAvatar, className)} ... />
|
||||
);
|
||||
if (!frame) return avatar;
|
||||
return (
|
||||
<div className={css.AvatarFrameContainer} data-frame={frame}>
|
||||
{avatar}
|
||||
<div className={css.AvatarFrameEffect} />
|
||||
</div>
|
||||
);
|
||||
```
|
||||
* **2. CSS (src/app/components/user-avatar/UserAvatar.css.ts):**
|
||||
Define animations like `rotate` or `pulse`. Use `position: absolute; inset: -4px` on the effect div to create a glowing ring around the avatar.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Priority 4 — Specialized Features
|
||||
|
||||
### P4-4 · Math / LaTeX Rendering
|
||||
@@ -108,28 +68,42 @@ This document provides exhaustive, low-level implementation details for the rema
|
||||
|
||||
## 🎨 Priority 5 — Gamer / Aesthetic / Customization
|
||||
|
||||
### P5-12 · Seasonal / Event Themes
|
||||
**Mechanism:** Date-aware global overlays.
|
||||
### P5-1 · Custom Accent Color Picker (Non-TDS only)
|
||||
**Mechanism:** Dynamic CSS variable injection.
|
||||
|
||||
* **1. Component (src/app/components/seasonal/SeasonalEffect.tsx):**
|
||||
```tsx
|
||||
const isHalloween = now.month === 9 && now.day >= 15; // etc.
|
||||
if (!isHalloween) return null;
|
||||
return <div className={css.HalloweenSpiderWeb} />;
|
||||
* **1. Setting (src/app/state/settings.ts):**
|
||||
Add `customAccentColor: string` (hex).
|
||||
* **2. Manager (src/app/pages/ThemeManager.tsx):**
|
||||
Inside the `useEffect` that monitors theme changes:
|
||||
```typescript
|
||||
if (!lotusTerminal && customAccentColor) {
|
||||
document.documentElement.style.setProperty('--lt-accent-orange', customAccentColor);
|
||||
// Also derive a 'glow' version (e.g. 50% opacity)
|
||||
document.documentElement.style.setProperty('--lt-accent-orange-glow', `${customAccentColor}80`);
|
||||
}
|
||||
```
|
||||
* **2. Styles (src/app/styles/Animations.css.ts):**
|
||||
Use `repeating-linear-gradient` for spider webs or `keyframes` for falling snow.
|
||||
* **3. Mounting:** Place at the very end of `App.tsx` (above the `LotusToastContainer`) with `pointer-events: none`.
|
||||
* **3. UI (src/app/features/settings/general/General.tsx):**
|
||||
Use a `<Input type="color">` component. Hide this section if `lotusTerminal` is `true`.
|
||||
|
||||
### P5-13 · Avatar Frame / Border Decorations
|
||||
**Mechanism:** Static variant of P5-14.
|
||||
### P5-40 · Desktop — Proactive Update Notifications (Tauri)
|
||||
**Mechanism:** Global Background Check via `useTauriUpdater`.
|
||||
|
||||
* **Implementation:** Use a simple `border` or `box-shadow` in `UserAvatar.tsx` based on the `io.lotus.avatar_frame` account data. Unlike the animated version, this should avoid `keyframes` to save CPU on long member lists.
|
||||
* **Objective:** Alert users to app updates without requiring a manual check in settings.
|
||||
* **Key Files:**
|
||||
* `src/app/hooks/useTauriUpdater.ts`: Logic source.
|
||||
* `src/app/pages/client/ClientNonUIFeatures.tsx`: Background mounting point.
|
||||
* `src/app/features/toast/LotusToastContainer.tsx`: UI for notification.
|
||||
* **Implementation:**
|
||||
1. Create a `TauriUpdateFeature` component.
|
||||
2. Use `useTauriUpdater()` to get the `check` function and `status`.
|
||||
3. In a `useEffect`, call `check()` on mount and then on a `setInterval` (e.g., every 12 hours).
|
||||
4. Watch the `status`. When it transitions to `{ state: 'available', version: '...' }`, trigger an in-app **Lotus Toast**.
|
||||
5. The toast should say "Lotus Chat v[version] is available!" with an "Update" button that calls the `install()` function from the hook.
|
||||
6. **Persistence:** Store the `lastCheck` timestamp in `localStorage` to ensure the background check doesn't fire redundant commands every time the user refreshes or re-opens the app.
|
||||
|
||||
### P5-2 · Additional Color Theme Presets
|
||||
**Mechanism:** Standard vanilla-extract theme multiplication.
|
||||
---
|
||||
|
||||
* **Implementation:** Replicate the `lotusTerminalTheme` object in `src/colors.css.ts` for each new theme (Cyberpunk, Ocean, etc.). Ensure each theme defines all 50+ tokens required by the `folds` library and TDS.
|
||||
## 🔊 Audio & Communications
|
||||
|
||||
### P5-15 · In-Call Soundboard
|
||||
**Mechanism:** Local-to-Global Audio Bridge.
|
||||
@@ -183,6 +157,7 @@ This document provides exhaustive, low-level implementation details for the rema
|
||||
* In `CallControl.ts`, intercept the `localStream`.
|
||||
* Pass the stream through the Worklet.
|
||||
* Crucially, you must ensure that the processed stream is used by the `RTCPeerConnection` within the Element Call iframe.
|
||||
|
||||
### P5-31 · Granular Voice & Screenshare Quality Controls
|
||||
**Mechanism:** WebRTC Encoding Parameters + Backend Quality Guard.
|
||||
|
||||
@@ -220,15 +195,3 @@ This document provides exhaustive, low-level implementation details for the rema
|
||||
* **Challenges:**
|
||||
* **LiveKit Compatibility:** Ensuring the SFU doesn't over-compress a high-bitrate stream from a "Pro" user.
|
||||
* **Network Stability:** High bitrates (512kbps audio + 60fps 1080p video) require significant upstream bandwidth. Implement a "Network Warning" UI if packets are dropped.
|
||||
|
||||
### P5-34 · User-to-User Private Notes
|
||||
**Mechanism:** Encrypted account data map.
|
||||
|
||||
* **Objective:** Private, cross-session notes about other users.
|
||||
* **Key Files:**
|
||||
* `src/app/hooks/useUserNotes.ts`: New hook for CRUD operations.
|
||||
* `src/app/components/user-profile/UserRoomProfile.tsx`: UI site.
|
||||
* **Implementation:**
|
||||
1. Store as a single map in `io.lotus.user_notes` account data: `{ "@alice:server": "Cool dev", "@bob:server": "Needs moderation" }`.
|
||||
2. Wrap the entire map in E2EE if the client supports local account data encryption, or rely on standard Matrix account data privacy.
|
||||
3. Add a "Notes" tab or textarea to the user profile popout.
|
||||
|
||||
@@ -7,16 +7,12 @@ import {
|
||||
animFloatUp,
|
||||
animBob,
|
||||
animTasselSway,
|
||||
animGlitch,
|
||||
animGlitchColor,
|
||||
animGlitchScan,
|
||||
animBurst,
|
||||
animWarp,
|
||||
animScanline,
|
||||
animPixelBlink,
|
||||
animGoldShimmer,
|
||||
animCloverDrift,
|
||||
animEarthLeafDrift,
|
||||
animWarp,
|
||||
animScanline,
|
||||
animPixelBlink,
|
||||
} from './Seasonal.css';
|
||||
|
||||
export type SeasonTheme =
|
||||
@@ -98,7 +94,7 @@ function HalloweenOverlay({ reduced }: { reduced: boolean }) {
|
||||
particles.map((_, i) => {
|
||||
const isOrange = i % 3 === 0;
|
||||
const size = 4 + (i % 3) * 2;
|
||||
const left = ((i * 4597 + 137) % 100);
|
||||
const left = (i * 4597 + 137) % 100;
|
||||
const duration = 8 + (i % 7) * 1.5;
|
||||
const delay = (i * 0.45) % 7;
|
||||
return (
|
||||
@@ -141,7 +137,7 @@ function ChristmasOverlay({ reduced }: { reduced: boolean }) {
|
||||
{!reduced &&
|
||||
flakes.map((_, i) => {
|
||||
const size = 3 + (i % 4) * 2;
|
||||
const left = ((i * 3571 + 251) % 100);
|
||||
const left = (i * 3571 + 251) % 100;
|
||||
const duration = 10 + (i % 8) * 2;
|
||||
const delay = (i * 0.55) % 10;
|
||||
const drift = ((i % 5) - 2) * 12;
|
||||
@@ -168,17 +164,10 @@ function ChristmasOverlay({ reduced }: { reduced: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Replaced flashing burst rays with gentle falling confetti
|
||||
function NewYearOverlay({ reduced }: { reduced: boolean }) {
|
||||
const bursts = [
|
||||
{ x: 20, y: 25, color: '#ffd700', delay: 0 },
|
||||
{ x: 75, y: 15, color: '#ff4466', delay: 1.2 },
|
||||
{ x: 50, y: 35, color: '#00d4ff', delay: 2.4 },
|
||||
{ x: 15, y: 60, color: '#ffd700', delay: 3.6 },
|
||||
{ x: 85, y: 45, color: '#aa44ff', delay: 0.8 },
|
||||
{ x: 40, y: 20, color: '#ff8800', delay: 2.0 },
|
||||
];
|
||||
|
||||
const petals = Array.from({ length: 8 });
|
||||
const confetti = Array.from({ length: 24 });
|
||||
const colors = ['#ffd700', '#ff4466', '#00d4ff', '#aa44ff', '#ff8800', '#ffffff'];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -187,50 +176,48 @@ function NewYearOverlay({ reduced }: { reduced: boolean }) {
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(10,5,0,0.15)',
|
||||
backgroundColor: 'rgba(10,5,0,0.10)',
|
||||
backgroundImage:
|
||||
'radial-gradient(ellipse at 50% 50%, rgba(255,200,0,0.04) 0%, transparent 70%)',
|
||||
}}
|
||||
/>
|
||||
{/* Gentle falling confetti */}
|
||||
{!reduced &&
|
||||
bursts.map((b, bi) =>
|
||||
petals.map((_, pi) => {
|
||||
const angle = (pi / petals.length) * 360;
|
||||
const dist = 80 + (pi % 3) * 30;
|
||||
const duration = 1.6 + (bi % 3) * 0.4;
|
||||
const period = 3.5 + bi * 0.8;
|
||||
return (
|
||||
<div
|
||||
key={`${bi}-${pi}`}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${b.x}%`,
|
||||
top: `${b.y}%`,
|
||||
width: `${dist}px`,
|
||||
height: '2px',
|
||||
backgroundColor: b.color,
|
||||
boxShadow: `0 0 6px ${b.color}`,
|
||||
transformOrigin: '0 50%',
|
||||
transform: `rotate(${angle}deg)`,
|
||||
animation: `${animBurst} ${duration}s ease-out ${b.delay + pi * 0.05}s ${period}s infinite`,
|
||||
borderRadius: '1px',
|
||||
opacity: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
{/* Gold shimmer overlay */}
|
||||
confetti.map((_, i) => {
|
||||
const c = colors[i % colors.length];
|
||||
const left = (i * 4597 + 137) % 100;
|
||||
const size = 3 + (i % 3) * 2;
|
||||
const duration = 8 + (i % 7) * 1.5;
|
||||
const delay = (i * 0.4) % 8;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
left: `${left}%`,
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
borderRadius: i % 2 === 0 ? '50%' : '1px',
|
||||
backgroundColor: c,
|
||||
boxShadow: `0 0 ${size + 2}px ${c}`,
|
||||
animation: `${animSeasonFall} ${duration}s ease-in ${delay}s infinite`,
|
||||
opacity: 0.7 + (i % 3) * 0.1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* Slow gold shimmer */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundImage:
|
||||
'linear-gradient(105deg, transparent 30%, rgba(255,215,0,0.06) 50%, transparent 70%)',
|
||||
'linear-gradient(105deg, transparent 30%, rgba(255,215,0,0.05) 50%, transparent 70%)',
|
||||
backgroundSize: '200% 100%',
|
||||
animation: reduced ? 'none' : `${animGoldShimmer} 4s linear infinite`,
|
||||
animation: reduced ? 'none' : `${animGoldShimmer} 5s linear infinite`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -259,11 +246,11 @@ function AutumnOverlay({ reduced }: { reduced: boolean }) {
|
||||
/>
|
||||
{!reduced &&
|
||||
leaves.map((_, i) => {
|
||||
const left = ((i * 5381 + 179) % 100);
|
||||
const left = (i * 5381 + 179) % 100;
|
||||
const duration = 12 + (i % 6) * 2;
|
||||
const delay = (i * 0.65) % 12;
|
||||
const size = 10 + (i % 4) * 4;
|
||||
const color = colors[i % colors.length];
|
||||
const col = colors[i % colors.length];
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
@@ -275,8 +262,8 @@ function AutumnOverlay({ reduced }: { reduced: boolean }) {
|
||||
width: `${size}px`,
|
||||
height: `${size * 0.7}px`,
|
||||
borderRadius: '50% 0 50% 0',
|
||||
backgroundColor: color,
|
||||
boxShadow: `0 0 4px ${color}`,
|
||||
backgroundColor: col,
|
||||
boxShadow: `0 0 4px ${col}`,
|
||||
animation: `${animLeafFall} ${duration}s ease-in ${delay}s infinite`,
|
||||
}}
|
||||
/>
|
||||
@@ -286,115 +273,104 @@ function AutumnOverlay({ reduced }: { reduced: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Replaced aggressive glitch with playful confetti rain
|
||||
function AprilFoolsOverlay({ reduced }: { reduced: boolean }) {
|
||||
const particles = Array.from({ length: 20 });
|
||||
const symbols = ['?', '!', '¿', '‽', '?', '!'];
|
||||
const colors = [
|
||||
'rgba(255,80,80,0.55)',
|
||||
'rgba(255,200,0,0.55)',
|
||||
'rgba(80,200,80,0.55)',
|
||||
'rgba(80,80,255,0.55)',
|
||||
'rgba(200,80,200,0.55)',
|
||||
'rgba(80,200,200,0.55)',
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* RGB channel separation layers */}
|
||||
{/* Subtle rainbow stripe along top edge */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(255,0,0,0.04)',
|
||||
transform: 'translateX(2px)',
|
||||
mixBlendMode: 'multiply',
|
||||
animation: reduced ? 'none' : `${animGlitch} 5s step-end infinite`,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
backgroundImage:
|
||||
'linear-gradient(90deg, rgba(255,0,0,0.4), rgba(255,165,0,0.4), rgba(255,255,0,0.4), rgba(0,200,0,0.4), rgba(0,0,255,0.4), rgba(128,0,128,0.4))',
|
||||
opacity: 0.7,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(0,255,255,0.04)',
|
||||
transform: 'translateX(-2px)',
|
||||
mixBlendMode: 'multiply',
|
||||
animation: reduced ? 'none' : `${animGlitch} 5s step-end 0.3s infinite`,
|
||||
}}
|
||||
/>
|
||||
{/* Color corruption */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
animation: reduced ? 'none' : `${animGlitchColor} 7s step-end infinite`,
|
||||
}}
|
||||
/>
|
||||
{/* Sweeping scanline */}
|
||||
{!reduced && (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
backgroundColor: 'rgba(0,255,136,0.35)',
|
||||
boxShadow: '0 0 8px rgba(0,255,136,0.5)',
|
||||
animation: `${animGlitchScan} 2.8s linear infinite`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* "ERROR" watermark */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%) rotate(-15deg)',
|
||||
fontSize: '80px',
|
||||
fontWeight: 900,
|
||||
fontFamily: 'monospace',
|
||||
color: 'rgba(255,0,0,0.07)',
|
||||
letterSpacing: '0.1em',
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
SIGNAL LOST
|
||||
</div>
|
||||
{/* Gentle falling punctuation symbols */}
|
||||
{!reduced &&
|
||||
particles.map((_, i) => {
|
||||
const left = (i * 5381 + 179) % 100;
|
||||
const duration = 11 + (i % 5) * 2.5;
|
||||
const delay = (i * 0.55) % 10;
|
||||
const col = colors[i % colors.length];
|
||||
const sym = symbols[i % symbols.length];
|
||||
const size = 12 + (i % 3) * 5;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-20px',
|
||||
left: `${left}%`,
|
||||
fontSize: `${size}px`,
|
||||
color: col,
|
||||
fontWeight: 700,
|
||||
fontFamily: 'monospace',
|
||||
animation: `${animSeasonFall} ${duration}s ease-in ${delay}s infinite`,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
{sym}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Reduced to 4 lanterns, subtler tint and shimmer
|
||||
function LunarNewYearOverlay({ reduced }: { reduced: boolean }) {
|
||||
const lanterns = Array.from({ length: 9 });
|
||||
const lanterns = Array.from({ length: 4 }); // was 9
|
||||
return (
|
||||
<>
|
||||
{/* Silk-like texture overlay */}
|
||||
{/* Very subtle red silk tint */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(140,0,0,0.08)',
|
||||
backgroundColor: 'rgba(140,0,0,0.05)',
|
||||
backgroundImage: [
|
||||
'repeating-linear-gradient(45deg, rgba(200,20,0,0.03) 0px, rgba(200,20,0,0.03) 1px, transparent 1px, transparent 8px)',
|
||||
'repeating-linear-gradient(135deg, rgba(200,20,0,0.03) 0px, rgba(200,20,0,0.03) 1px, transparent 1px, transparent 8px)',
|
||||
'repeating-linear-gradient(45deg, rgba(200,20,0,0.015) 0px, rgba(200,20,0,0.015) 1px, transparent 1px, transparent 8px)',
|
||||
'repeating-linear-gradient(135deg, rgba(200,20,0,0.015) 0px, rgba(200,20,0,0.015) 1px, transparent 1px, transparent 8px)',
|
||||
].join(','),
|
||||
}}
|
||||
/>
|
||||
{/* Gold shimmer sweep */}
|
||||
{/* Slow gold shimmer */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundImage:
|
||||
'linear-gradient(100deg, transparent 25%, rgba(255,200,0,0.07) 45%, rgba(255,220,50,0.1) 50%, rgba(255,200,0,0.07) 55%, transparent 75%)',
|
||||
'linear-gradient(100deg, transparent 25%, rgba(255,200,0,0.05) 45%, rgba(255,220,50,0.07) 50%, rgba(255,200,0,0.05) 55%, transparent 75%)',
|
||||
backgroundSize: '300% 100%',
|
||||
animation: reduced ? 'none' : `${animGoldShimmer} 5s linear infinite`,
|
||||
animation: reduced ? 'none' : `${animGoldShimmer} 8s linear infinite`,
|
||||
}}
|
||||
/>
|
||||
{/* Floating paper lanterns */}
|
||||
{/* 4 floating lanterns */}
|
||||
{lanterns.map((_, i) => {
|
||||
const left = 5 + ((i * 4603 + 311) % 90);
|
||||
const top = 8 + ((i * 2311 + 97) % 55);
|
||||
const left = 10 + ((i * 4603 + 311) % 75);
|
||||
const top = 10 + ((i * 2311 + 97) % 50);
|
||||
const duration = 3.5 + (i % 4) * 0.7;
|
||||
const delay = i * 0.5;
|
||||
const delay = i * 0.9;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
@@ -403,10 +379,10 @@ function LunarNewYearOverlay({ reduced }: { reduced: boolean }) {
|
||||
position: 'absolute',
|
||||
left: `${left}%`,
|
||||
top: `${top}%`,
|
||||
animation: reduced ? 'none' : `${animBob} ${duration}s ease-in-out ${delay}s infinite`,
|
||||
animation:
|
||||
reduced ? 'none' : `${animBob} ${duration}s ease-in-out ${delay}s infinite`,
|
||||
}}
|
||||
>
|
||||
{/* Lantern top cap */}
|
||||
<div
|
||||
style={{
|
||||
width: '18px',
|
||||
@@ -417,7 +393,6 @@ function LunarNewYearOverlay({ reduced }: { reduced: boolean }) {
|
||||
boxShadow: '0 0 4px rgba(255,215,0,0.6)',
|
||||
}}
|
||||
/>
|
||||
{/* Lantern body */}
|
||||
<div
|
||||
style={{
|
||||
width: '24px',
|
||||
@@ -429,7 +404,6 @@ function LunarNewYearOverlay({ reduced }: { reduced: boolean }) {
|
||||
margin: '1px auto',
|
||||
}}
|
||||
/>
|
||||
{/* Lantern bottom cap */}
|
||||
<div
|
||||
style={{
|
||||
width: '18px',
|
||||
@@ -439,14 +413,16 @@ function LunarNewYearOverlay({ reduced }: { reduced: boolean }) {
|
||||
margin: '0 auto',
|
||||
}}
|
||||
/>
|
||||
{/* Tassel */}
|
||||
<div
|
||||
style={{
|
||||
width: '2px',
|
||||
height: '14px',
|
||||
backgroundColor: '#ffd700',
|
||||
margin: '0 auto',
|
||||
animation: reduced ? 'none' : `${animTasselSway} ${duration * 0.6}s ease-in-out ${delay}s infinite`,
|
||||
animation:
|
||||
reduced
|
||||
? 'none'
|
||||
: `${animTasselSway} ${duration * 0.6}s ease-in-out ${delay}s infinite`,
|
||||
transformOrigin: 'top center',
|
||||
}}
|
||||
/>
|
||||
@@ -482,7 +458,7 @@ function ValentinesOverlay({ reduced }: { reduced: boolean }) {
|
||||
const duration = 9 + (i % 6) * 1.8;
|
||||
const delay = (i * 0.6) % 9;
|
||||
const size = 14 + (i % 4) * 5;
|
||||
const color = colors[i % colors.length];
|
||||
const col = colors[i % colors.length];
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
@@ -492,7 +468,7 @@ function ValentinesOverlay({ reduced }: { reduced: boolean }) {
|
||||
bottom: '-20px',
|
||||
left: `${left}%`,
|
||||
fontSize: `${size}px`,
|
||||
color,
|
||||
color: col,
|
||||
filter: 'drop-shadow(0 0 4px rgba(255,100,140,0.4))',
|
||||
animation: `${animFloatUp} ${duration}s ease-in ${delay}s infinite`,
|
||||
userSelect: 'none',
|
||||
@@ -521,7 +497,6 @@ function StPatricksOverlay({ reduced }: { reduced: boolean }) {
|
||||
].join(','),
|
||||
}}
|
||||
/>
|
||||
{/* Moving metallic gold shimmer on the accent border at top */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
@@ -538,7 +513,7 @@ function StPatricksOverlay({ reduced }: { reduced: boolean }) {
|
||||
/>
|
||||
{!reduced &&
|
||||
clovers.map((_, i) => {
|
||||
const left = ((i * 4129 + 223) % 100);
|
||||
const left = (i * 4129 + 223) % 100;
|
||||
const duration = 14 + (i % 6) * 2;
|
||||
const delay = (i * 0.7) % 12;
|
||||
const size = 14 + (i % 3) * 6;
|
||||
@@ -581,7 +556,6 @@ function EarthDayOverlay({ reduced }: { reduced: boolean }) {
|
||||
].join(','),
|
||||
}}
|
||||
/>
|
||||
{/* Vine line along the left edge */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
@@ -626,7 +600,6 @@ function DeepSpaceOverlay({ reduced }: { reduced: boolean }) {
|
||||
const stars = Array.from({ length: 24 });
|
||||
return (
|
||||
<>
|
||||
{/* Deep space ambient */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
@@ -640,7 +613,6 @@ function DeepSpaceOverlay({ reduced }: { reduced: boolean }) {
|
||||
].join(','),
|
||||
}}
|
||||
/>
|
||||
{/* Warp streak particles emanating from center */}
|
||||
{!reduced &&
|
||||
stars.map((_, i) => {
|
||||
const angle = (i / stars.length) * 360;
|
||||
@@ -648,7 +620,11 @@ function DeepSpaceOverlay({ reduced }: { reduced: boolean }) {
|
||||
const delay = (i * 0.18) % 2.5;
|
||||
const period = 3 + (i % 4) * 0.5;
|
||||
const size = 1 + (i % 3);
|
||||
const colors = ['rgba(200,180,255,0.9)', 'rgba(150,200,255,0.8)', 'rgba(255,255,255,0.7)'];
|
||||
const starColors = [
|
||||
'rgba(200,180,255,0.9)',
|
||||
'rgba(150,200,255,0.8)',
|
||||
'rgba(255,255,255,0.7)',
|
||||
];
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
@@ -659,10 +635,10 @@ function DeepSpaceOverlay({ reduced }: { reduced: boolean }) {
|
||||
top: '50%',
|
||||
width: `${80 + i * 6}px`,
|
||||
height: `${size}px`,
|
||||
backgroundColor: colors[i % colors.length],
|
||||
backgroundColor: starColors[i % starColors.length],
|
||||
transformOrigin: '0 50%',
|
||||
transform: `rotate(${angle}deg)`,
|
||||
boxShadow: `0 0 ${size * 2}px ${colors[i % colors.length]}`,
|
||||
boxShadow: `0 0 ${size * 2}px ${starColors[i % starColors.length]}`,
|
||||
animation: `${animWarp} ${duration}s ease-out ${delay}s ${period}s infinite`,
|
||||
opacity: 0,
|
||||
}}
|
||||
@@ -676,7 +652,6 @@ function DeepSpaceOverlay({ reduced }: { reduced: boolean }) {
|
||||
function ArcadeOverlay({ reduced }: { reduced: boolean }) {
|
||||
return (
|
||||
<>
|
||||
{/* CRT scanlines */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
@@ -687,7 +662,6 @@ function ArcadeOverlay({ reduced }: { reduced: boolean }) {
|
||||
animation: reduced ? 'none' : `${animScanline} 3s ease-in-out infinite`,
|
||||
}}
|
||||
/>
|
||||
{/* Pixel corner decorations */}
|
||||
{(['0,0', '0,auto', 'auto,0', 'auto,auto'] as const).map((corner, i) => {
|
||||
const [t, b] = corner.split(',');
|
||||
return (
|
||||
@@ -712,7 +686,6 @@ function ArcadeOverlay({ reduced }: { reduced: boolean }) {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* "INSERT COIN" prompt */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
@@ -731,7 +704,6 @@ function ArcadeOverlay({ reduced }: { reduced: boolean }) {
|
||||
>
|
||||
— INSERT COIN —
|
||||
</div>
|
||||
{/* Vignette */}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
@@ -745,29 +717,40 @@ function ArcadeOverlay({ reduced }: { reduced: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Wrapper ──────────────────────────────────────────────────────────────────
|
||||
// ─── Overlay content map (shared between SeasonalOverlay and SeasonalPreview) ──
|
||||
|
||||
function SeasonalOverlay({
|
||||
theme,
|
||||
reduced,
|
||||
}: {
|
||||
theme: SeasonTheme;
|
||||
reduced: boolean;
|
||||
}) {
|
||||
const overlayMap: Record<SeasonTheme, React.ReactNode> = {
|
||||
halloween: <HalloweenOverlay reduced={reduced} />,
|
||||
christmas: <ChristmasOverlay reduced={reduced} />,
|
||||
newyear: <NewYearOverlay reduced={reduced} />,
|
||||
autumn: <AutumnOverlay reduced={reduced} />,
|
||||
aprilfools: <AprilFoolsOverlay reduced={reduced} />,
|
||||
lunar: <LunarNewYearOverlay reduced={reduced} />,
|
||||
valentines: <ValentinesOverlay reduced={reduced} />,
|
||||
stpatricks: <StPatricksOverlay reduced={reduced} />,
|
||||
earthday: <EarthDayOverlay reduced={reduced} />,
|
||||
deepspace: <DeepSpaceOverlay reduced={reduced} />,
|
||||
arcade: <ArcadeOverlay reduced={reduced} />,
|
||||
};
|
||||
function buildOverlayContent(theme: SeasonTheme, reduced: boolean): React.ReactNode {
|
||||
switch (theme) {
|
||||
case 'halloween':
|
||||
return <HalloweenOverlay reduced={reduced} />;
|
||||
case 'christmas':
|
||||
return <ChristmasOverlay reduced={reduced} />;
|
||||
case 'newyear':
|
||||
return <NewYearOverlay reduced={reduced} />;
|
||||
case 'autumn':
|
||||
return <AutumnOverlay reduced={reduced} />;
|
||||
case 'aprilfools':
|
||||
return <AprilFoolsOverlay reduced={reduced} />;
|
||||
case 'lunar':
|
||||
return <LunarNewYearOverlay reduced={reduced} />;
|
||||
case 'valentines':
|
||||
return <ValentinesOverlay reduced={reduced} />;
|
||||
case 'stpatricks':
|
||||
return <StPatricksOverlay reduced={reduced} />;
|
||||
case 'earthday':
|
||||
return <EarthDayOverlay reduced={reduced} />;
|
||||
case 'deepspace':
|
||||
return <DeepSpaceOverlay reduced={reduced} />;
|
||||
case 'arcade':
|
||||
return <ArcadeOverlay reduced={reduced} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Full-screen overlay (fixed position, used in App) ────────────────────────
|
||||
|
||||
function SeasonalOverlay({ theme, reduced }: { theme: SeasonTheme; reduced: boolean }) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
@@ -779,11 +762,30 @@ function SeasonalOverlay({
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{overlayMap[theme]}
|
||||
{buildOverlayContent(theme, reduced)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Preview overlay (absolute position, contained in a card) ─────────────────
|
||||
|
||||
/**
|
||||
* Renders the ambient (reduced-motion) version of a seasonal overlay inside
|
||||
* a parent container. The parent must have `position: relative; overflow: hidden`.
|
||||
*/
|
||||
export function SeasonalPreview({ theme }: { theme: SeasonTheme }) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{ position: 'absolute', inset: 0, overflow: 'hidden', pointerEvents: 'none' }}
|
||||
>
|
||||
{buildOverlayContent(theme, true)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main exported component ──────────────────────────────────────────────────
|
||||
|
||||
export function SeasonalEffect() {
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const reduced =
|
||||
|
||||
@@ -17,6 +17,8 @@ import { RoomViewFollowing, RoomViewFollowingPlaceholder } from './RoomViewFollo
|
||||
import { Page } from '../../components/page';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useTheme, ThemeKind } from '../../hooks/useTheme';
|
||||
import { getChatBg } from '../lotus/chatBackground';
|
||||
import { useKeyDown } from '../../hooks/useKeyDown';
|
||||
import { editableActiveElement } from '../../utils/dom';
|
||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||
@@ -59,7 +61,9 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
||||
const roomViewRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>;
|
||||
const [chatBackground] = useSetting(settingsAtom, 'chatBackground');
|
||||
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
|
||||
const [glassmorphismSidebar] = useSetting(settingsAtom, 'glassmorphismSidebar');
|
||||
const [pauseAnimations] = useSetting(settingsAtom, 'pauseAnimations');
|
||||
const theme = useTheme();
|
||||
const isDark = theme.kind === ThemeKind.Dark;
|
||||
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
|
||||
@@ -93,13 +97,14 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
||||
),
|
||||
);
|
||||
|
||||
// document.body always carries the active background (set by SidebarNav).
|
||||
// Make Page transparent so the body background shows through the chat area.
|
||||
// When no background is active, Page keeps its default theme surface color.
|
||||
// Apply the background directly to Page so it overrides PageRoot's opaque
|
||||
// Background.Container color. SidebarNav mirrors it onto document.body separately
|
||||
// so the glassmorphism sidebar can blur through it.
|
||||
const chatBgStyle = useMemo(() => {
|
||||
const hasBg = chatBackground !== 'none' || glassmorphismSidebar || lotusTerminal;
|
||||
return hasBg ? ({ background: 'transparent' } as React.CSSProperties) : {};
|
||||
}, [chatBackground, glassmorphismSidebar, lotusTerminal]);
|
||||
if (chatBackground !== 'none') return getChatBg(chatBackground, isDark, pauseAnimations);
|
||||
if (lotusTerminal) return getChatBg('tactical', isDark, pauseAnimations);
|
||||
return {};
|
||||
}, [chatBackground, lotusTerminal, isDark, pauseAnimations]);
|
||||
|
||||
return (
|
||||
<Page ref={roomViewRef} style={chatBgStyle}>
|
||||
|
||||
@@ -42,8 +42,13 @@ import {
|
||||
DateFormat,
|
||||
MessageLayout,
|
||||
MessageSpacing,
|
||||
Settings,
|
||||
settingsAtom,
|
||||
} from '../../../state/settings';
|
||||
import {
|
||||
SeasonalPreview,
|
||||
SeasonTheme,
|
||||
} from '../../../components/seasonal/SeasonalEffect';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { KeySymbol } from '../../../utils/key-symbol';
|
||||
import { isMacOS } from '../../../utils/user-agent';
|
||||
@@ -488,32 +493,22 @@ function Appearance() {
|
||||
/>
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="200"
|
||||
>
|
||||
<SettingTile
|
||||
title="Seasonal Theme"
|
||||
description="Decorative overlays that activate automatically on holidays and events, or choose one manually."
|
||||
after={
|
||||
<SettingsSelect
|
||||
value={seasonalThemeOverride ?? 'auto'}
|
||||
onChange={(v) => setSeasonalThemeOverride(v as typeof seasonalThemeOverride)}
|
||||
options={[
|
||||
{ value: 'auto', label: '🗓 Auto (date-based)' },
|
||||
{ value: 'off', label: 'Off' },
|
||||
{ value: 'newyear', label: '🎆 New Year' },
|
||||
{ value: 'lunar', label: '🏮 Lunar New Year' },
|
||||
{ value: 'valentines', label: '💖 Valentine\'s Day' },
|
||||
{ value: 'stpatricks', label: '🍀 St. Patrick\'s Day' },
|
||||
{ value: 'aprilfools', label: '🃏 April Fool\'s Day' },
|
||||
{ value: 'earthday', label: '🌱 Earth Day' },
|
||||
{ value: 'autumn', label: '🍂 Autumn' },
|
||||
{ value: 'halloween', label: '🎃 Halloween' },
|
||||
{ value: 'christmas', label: '❄️ Christmas' },
|
||||
{ value: 'arcade', label: '👾 Retro Arcade Day' },
|
||||
{ value: 'deepspace', label: '🚀 Deep Space Week' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
description="Decorative overlays for holidays and events. Preview below — click to select."
|
||||
/>
|
||||
<Box style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}>
|
||||
<SeasonalBgGrid
|
||||
value={seasonalThemeOverride ?? 'auto'}
|
||||
onChange={(v) => setSeasonalThemeOverride(v)}
|
||||
/>
|
||||
</Box>
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
@@ -1351,6 +1346,89 @@ function Calls() {
|
||||
);
|
||||
}
|
||||
|
||||
const SEASONAL_OPTIONS: { value: Settings['seasonalThemeOverride']; label: string; emoji: string }[] =
|
||||
[
|
||||
{ value: 'auto', label: 'Auto', emoji: '🗓' },
|
||||
{ value: 'off', label: 'Off', emoji: '×' },
|
||||
{ value: 'newyear', label: 'New Year', emoji: '🎆' },
|
||||
{ value: 'lunar', label: 'Lunar New Year', emoji: '🏮' },
|
||||
{ value: 'valentines', label: "Valentine's", emoji: '💖' },
|
||||
{ value: 'stpatricks', label: "St. Patrick's", emoji: '🍀' },
|
||||
{ value: 'aprilfools', label: 'April Fools', emoji: '?' },
|
||||
{ value: 'earthday', label: 'Earth Day', emoji: '🌱' },
|
||||
{ value: 'autumn', label: 'Autumn', emoji: '🍂' },
|
||||
{ value: 'halloween', label: 'Halloween', emoji: '🎃' },
|
||||
{ value: 'christmas', label: 'Christmas', emoji: '❄️' },
|
||||
{ value: 'arcade', label: 'Arcade Day', emoji: '👾' },
|
||||
{ value: 'deepspace', label: 'Deep Space', emoji: '🚀' },
|
||||
];
|
||||
|
||||
function SeasonalBgGrid({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: Settings['seasonalThemeOverride'];
|
||||
onChange: (v: Settings['seasonalThemeOverride']) => void;
|
||||
}) {
|
||||
return (
|
||||
<Box wrap="Wrap" gap="200">
|
||||
{SEASONAL_OPTIONS.map((opt) => {
|
||||
const selected = value === opt.value;
|
||||
const isSpecial = opt.value === 'auto' || opt.value === 'off';
|
||||
return (
|
||||
<Box key={opt.value} direction="Column" gap="100" style={{ alignItems: 'center' }}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={opt.label}
|
||||
aria-pressed={selected}
|
||||
onClick={() => onChange(opt.value)}
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'block',
|
||||
width: toRem(76),
|
||||
height: toRem(56),
|
||||
borderRadius: toRem(8),
|
||||
cursor: 'pointer',
|
||||
border: selected
|
||||
? `2px solid ${color.Critical.Main}`
|
||||
: '2px solid rgba(128,128,128,0.25)',
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#030508',
|
||||
}}
|
||||
>
|
||||
{!isSpecial && <SeasonalPreview theme={opt.value as SeasonTheme} />}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundImage:
|
||||
opt.value === 'auto'
|
||||
? 'linear-gradient(135deg, rgba(255,100,0,0.2), rgba(255,200,0,0.2), rgba(0,200,100,0.2), rgba(0,100,255,0.2))'
|
||||
: undefined,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
{isSpecial && (
|
||||
<span style={{ fontSize: '22px', opacity: opt.value === 'off' ? 0.4 : 1 }}>
|
||||
{opt.emoji}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
<Text size="T200" style={selected ? { color: color.Critical.Main } : undefined}>
|
||||
{opt.label}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatBgGrid() {
|
||||
const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground');
|
||||
const [pauseAnimations] = useSetting(settingsAtom, 'pauseAnimations');
|
||||
|
||||
Reference in New Issue
Block a user