diff --git a/LOTUS_BUGS.md b/LOTUS_BUGS.md index 12bbef668..de0aba624 100644 --- a/LOTUS_BUGS.md +++ b/LOTUS_BUGS.md @@ -32,12 +32,11 @@ This document tracks identified bugs, edge cases, and architectural discrepancie ### 1. Presence Updater Reverts Status Updates **File:** `src/app/hooks/usePresenceUpdater.ts` (Line 20) -**Status:** High Priority +**Status:** ✅ RESOLVED (June 2026) -* **Issue:** The `storedStatus` variable is captured once when the `useEffect` starts. -* **Impact:** If a user updates their status message in the Profile settings, the presence updater hook continues to use the *old* status message from its closure. Every time the user moves their mouse/activity, the hook sends `setOnline` with the stale status, reverting the user's change. -* **Recommended Fix:** - 1. Read the status from `localStorage` inside the `setOnline`/`setUnavailable` functions rather than at the top of the effect. +* **Issue:** The `storedStatus` variable was captured once when the `useEffect` started. +* **Impact:** If a user updated their status message in Profile Settings, the hook would continue broadcasting the old message on every activity event, silently reverting the change. +* **Fix Applied:** Replaced the single `localStorage.getItem` read with a `readStatus()` function called inside every `setOnline` and `setUnavailable` invocation, ensuring the current value is always used. ### 2. Audio Playback Rate Reset **File:** `src/app/components/message/content/AudioContent.tsx` (Line 97) @@ -57,11 +56,11 @@ This document tracks identified bugs, edge cases, and architectural discrepancie ### 4. Incorrect Ringing in Voice Rooms **File:** `src/app/components/CallEmbedProvider.tsx` -**Status:** Logic Bug +**Status:** ✅ RESOLVED (June 2026) -* **Issue:** Joining a static voice room (Public Space channel) triggers the "Incoming Call" ringing animation and sound. This should only occur for "StartedByUser" intents (DMs and Private Group Calls). -* **Root Cause:** The ringing logic does not distinguish between persistent voice rooms and user-initiated calls. -* **Recommended Fix:** Check `room.getJoinRule()` or check for the `m.space.parent` event. If the room is a public voice channel, suppress the ringing animation. +* **Issue:** Joining a static voice room (Public Space channel) triggered the "Incoming Call" ringing animation and sound. +* **Root Cause:** `getStateEvent(room, StateEvent.SpaceParent)` always returned `undefined` because `m.space.parent` events use the parent space's room ID as the state key, not an empty string. The ringing suppression check silently failed. +* **Fix Applied:** Replaced `getStateEvent` with `getStateEvents` (plural), which returns all events of a given type regardless of state key. A room with any `m.space.parent` event is correctly identified as a space channel and suppresses ringing. --- diff --git a/LOTUS_FEATURES.md b/LOTUS_FEATURES.md index 972cbf1e8..e44870473 100644 --- a/LOTUS_FEATURES.md +++ b/LOTUS_FEATURES.md @@ -10,20 +10,21 @@ Last updated: June 2026. 1. [Branding & Identity](#branding--identity) 2. [LotusGuild Terminal Design System (TDS) v1.2](#lotusguild-terminal-design-system-tds-v12) 3. [Animated Chat Backgrounds (P5-4)](#animated-chat-backgrounds-p5-4) -4. [Glassmorphism Sidebar (P5-3)](#glassmorphism-sidebar-p5-3) -5. [Night Light / Blue Light Filter (P5-5)](#night-light--blue-light-filter-p5-5) -6. [Voice / Video Call Improvements](#voice--video-call-improvements) -7. [Per-Message Read Receipts](#per-message-read-receipts) -8. [Delivery Status Indicators](#delivery-status-indicators) -9. [Messaging Enhancements](#messaging-enhancements) -10. [Presence](#presence) -11. [UX & Composer](#ux--composer) -12. [Room Customization](#room-customization) -13. [Moderation](#moderation) -14. [Notifications](#notifications) -15. [Server Integration](#server-integration) -16. [Infrastructure](#infrastructure) -17. [Key Custom Files](#key-custom-files) +4. [Seasonal Theme Overlays (P5-12)](#seasonal-theme-overlays-p5-12) +5. [Glassmorphism Sidebar (P5-3)](#glassmorphism-sidebar-p5-3) +6. [Night Light / Blue Light Filter (P5-5)](#night-light--blue-light-filter-p5-5) +7. [Voice / Video Call Improvements](#voice--video-call-improvements) +8. [Per-Message Read Receipts](#per-message-read-receipts) +9. [Delivery Status Indicators](#delivery-status-indicators) +10. [Messaging Enhancements](#messaging-enhancements) +11. [Presence](#presence) +12. [UX & Composer](#ux--composer) +13. [Room Customization](#room-customization) +14. [Moderation](#moderation) +15. [Notifications](#notifications) +16. [Server Integration](#server-integration) +17. [Infrastructure](#infrastructure) +18. [Key Custom Files](#key-custom-files) --- @@ -148,6 +149,16 @@ Strips all `animation` properties from the returned style object when either `pa A "Pause Background Animations" toggle is exposed in **Settings → Appearance**. The preference is persisted and read by `getChatBg()` at render time. +### Animation Improvements (June 2026) + +All five animated backgrounds were rewritten for smoother, more organic motion: + +- **Digital Rain** — added a phosphor glow flicker (`animRainGlowKeyframe`, 2.1 s) layered on top of the column scroll; stripe opacity increased for better visibility +- **Star Drift** — each of the three dot layers now moves by exactly its own tile width/height per cycle (`−130 px`, `−190 px`, `−260 px`), eliminating the visible seam on loop +- **Grid Pulse** — independent brightness oscillation (`animGridBrightnessKeyframe`, 3.3 s) runs alongside the size breathe (4 s) at a prime period ratio so they never synchronise +- **Aurora Flow** — four gradient layers now have individual `backgroundSize` values (`200%`, `250%`, `300%`, `220%`); the keyframe drives each layer through a distinct 5-stop path, replacing the robotic single back-and-forth +- **Fireflies** — glow pulse (`animFirefliesGlowKeyframe`, 2.3 s `filter: brightness`) and opacity blink (`animFirefliesBlinkKeyframe`, 1.7 s) added on top of the position drift; prime periods create unsynchronised bioluminescence + ### Files - `src/app/styles/Animations.css.ts` — vanilla-extract keyframe definitions @@ -155,6 +166,40 @@ A "Pause Background Animations" toggle is exposed in **Settings → Appearance** --- +## Seasonal Theme Overlays (P5-12) + +Decorative CSS-only overlays that activate automatically on holidays and events. Manually overrideable in **Settings → Appearance → Seasonal Theme**. + +### Themes + +| Theme | Window | Effect | +|---|---|---| +| 🎆 New Year | Dec 31–Jan 2 | Radial firework bursts in gold, red, cyan, purple; gold shimmer sweep | +| 🏮 Lunar New Year | Jan 22–Feb 5 | Floating paper lanterns bobbing; silk texture; gold shimmer accent | +| 💖 Valentine's Day | Feb 10–15 | ♥ hearts floating upward; soft pink ambient glow | +| 🍀 St. Patrick's Day | Mar 15–18 | ☘ clovers drifting down; gold metallic shimmer top border | +| 🃏 April Fool's | Apr 1 | Glitch overlay: RGB channel separation, hue-rotate spikes, scanline sweep, "SIGNAL LOST" watermark | +| 🌱 Earth Day | Apr 20–23 | 🌿🍃 leaf emoji drift; sage green ambient tint; vine accent on left edge | +| 🍂 Autumn | Sep 21–Oct 31 | Warm orange/amber leaf shapes rotating and falling | +| 👾 Arcade Day | Sep 12 | CRT scanlines; blinking pixel corner decorations; "INSERT COIN" prompt | +| 🚀 Deep Space Week | Oct 4–10 | Warp-speed star streaks radiating from screen centre; nebula purple/blue ambient | +| 🎃 Halloween | Oct 15–Nov 1 | Purple and orange glowing particles; SVG spider web in top-left corner; dark purple tint | +| ❄️ Christmas | Dec 10–Jan 2 | White dot snowfall in multiple layers at varied speeds | + +### Implementation + +- `SeasonalEffect` component mounted in `App.tsx` at `z-index: 9997` (below night light, above content) +- Auto-detection via `getActiveSeason(now: Date)` — themes checked in priority order (New Year > Valentine's > … > Autumn) +- `seasonalThemeOverride` setting: `'auto' | 'off' | ` — persisted in `settingsAtom` +- All particle animations gated on `prefers-reduced-motion: reduce` — ambient overlays (tints, textures, shimmer) remain active + +### Files + +- `src/app/components/seasonal/SeasonalEffect.tsx` — theme detection, date ranges, all overlay components +- `src/app/components/seasonal/Seasonal.css.ts` — vanilla-extract keyframes (fall, leaf, float-up, bob, glitch, burst, warp, scanline, shimmer, etc.) + +--- + ## Glassmorphism Sidebar (P5-3) An optional frosted-glass sidebar style toggled in **Settings → Appearance**. @@ -538,6 +583,12 @@ Applied in: - `@mention` autocomplete dropdown - Inbox / notifications panel +### Status Revert Bug Fix (June 2026) + +`usePresenceUpdater` previously captured the user's custom status message once via `localStorage.getItem` at effect initialization. When the user changed their status message in Profile Settings, subsequent automatic transitions back to `online` (e.g., returning from idle) would silently broadcast the old status message, reverting the custom status. + +Fixed by replacing the single read with a `readStatus()` function called inside every `setOnline` and `setUnavailable` invocation, so the current localStorage value is always used. + ### Document Title Unread Count The browser tab title updates to reflect unread state: @@ -565,6 +616,18 @@ When a user has `m.tz` set in their profile: Hook: `src/app/hooks/useLocalTime.ts` +### User-to-User Private Notes (P5-34) + +A private text note on any user's profile, visible only to the logged-in user and synced across all their devices. + +- Textarea in the user profile popout (below device sessions), shown only when viewing another user — never on your own profile +- Auto-saves 800 ms after the last keystroke with a "Saving…" indicator +- Character counter appears when fewer than 100 characters remain (max 500) +- Stored in `io.lotus.user_notes` account data as `{ [userId]: string }` — deletes the key when the note is cleared +- Reactive: updates immediately if account data arrives from another device mid-session + +Hook: `src/app/hooks/useUserNotes.ts` + --- ## UX & Composer diff --git a/README.md b/README.md index c592c49bf..c6fa3a11f 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,8 @@ The Lotus Chat logo (`lotus_chat.png`) is a derivative work based on the origina - LotusGuild Terminal Design System (TDS) — a CRT terminal-inspired dark theme - TDS light mode variant for daytime use - 20+ static chat background patterns -- 5 animated chat backgrounds: Digital Rain, Star Drift, Grid Pulse, Aurora Flow, Fireflies +- 5 animated chat backgrounds: Digital Rain, Star Drift, Grid Pulse, Aurora Flow, Fireflies (with improved per-layer looping, phosphor-flicker rain, fluid aurora sweep, and organic firefly bioluminescence) +- 11 seasonal & holiday theme overlays — Halloween, Christmas, New Year, Autumn, Valentine's Day, St. Patrick's Day, Earth Day, Lunar New Year, April Fools', Deep Space, and Retro Arcade; auto-selected by date with a manual override in Settings → Appearance - Toggle to pause background animations - Glassmorphism sidebar — frosted glass effect that lets the background show through - Night Light / blue light filter with an adjustable intensity slider @@ -69,10 +70,11 @@ The Lotus Chat logo (`lotus_chat.png`) is a derivative work based on the origina ### Presence & Profile - Discord-style presence selector: Online, Idle, Do Not Disturb, Invisible, or Auto -- Custom status message with emoji and an optional auto-clear timer +- Custom status message with emoji and an optional auto-clear timer (changing your status is never silently overwritten by activity events) - Colored presence ring on member avatars (green / yellow / red) - Profile fields for pronouns and timezone - When a user's timezone is set, their current local time appears in their profile +- Private notes on any user's profile — freeform text visible only to you, auto-saves and syncs across devices - Unread count shown in the browser tab title ### Moderation & Privacy diff --git a/src/app/components/CallEmbedProvider.tsx b/src/app/components/CallEmbedProvider.tsx index f45bbfc20..ed3183e67 100644 --- a/src/app/components/CallEmbedProvider.tsx +++ b/src/app/components/CallEmbedProvider.tsx @@ -54,7 +54,7 @@ import { getChatBg } from '../features/lotus/chatBackground'; import { useTheme, ThemeKind } from '../hooks/useTheme'; import { useSetting } from '../state/hooks/settings'; import { settingsAtom } from '../state/settings'; -import { getStateEvent, getMemberDisplayName } from '../utils/room'; +import { getStateEvent, getStateEvents, getMemberDisplayName } from '../utils/room'; import { StateEvent } from '../../types/matrix/room'; import { getPowersLevelFromMatrixEvent } from '../hooks/usePowerLevels'; import { getRoomCreatorsForRoomId } from '../hooks/useRoomCreators'; @@ -329,7 +329,10 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps) // Space voice channels and public rooms fire room-level RTC notifications // whenever anyone joins — ringing every member is incorrect behaviour. const isDirect = directs.has(room.roomId); - const isSpaceChild = !!getStateEvent(room, StateEvent.SpaceParent); + // m.space.parent uses the parent space ID as the state key, so getStateEvent + // (which defaults to stateKey='') always returns undefined. Use getStateEvents + // (no key filter) to detect any space parent relationship. + const isSpaceChild = getStateEvents(room, StateEvent.SpaceParent).length > 0; const joinRule = room.getJoinRule(); const isPrivateGroup = !isSpaceChild && diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 696d31c39..19a72d22d 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -1,5 +1,5 @@ import { Box, Button, config, Icon, IconButton, Icons, Spinner, Text, toRem } from 'folds'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { VerificationRequest } from 'matrix-js-sdk/lib/crypto-api'; import { AsyncState, AsyncStatus, useAsync } from '../../hooks/useAsyncCallback'; @@ -32,6 +32,7 @@ import { CreatorChip } from './CreatorChip'; import { getDirectCreatePath, withSearchParam } from '../../pages/pathUtils'; import { DirectCreateSearchParams } from '../../pages/paths'; import { useExtendedProfile } from '../../hooks/useExtendedProfile'; +import { useUserNotes, USER_NOTE_MAX_LENGTH } from '../../hooks/useUserNotes'; type VerifyDeviceButtonProps = { userId: string; @@ -207,6 +208,65 @@ function UserDeviceSessions({ userId }: UserDeviceSessionsProps) { ); } +function UserPrivateNotes({ userId }: { userId: string }) { + const { getNote, setNote } = useUserNotes(); + const [draft, setDraft] = useState(() => getNote(userId)); + const [saving, setSaving] = useState(false); + const saveTimer = useRef | undefined>(undefined); + + // Sync if account data arrives after mount + useEffect(() => { + setDraft(getNote(userId)); + }, [getNote, userId]); + + const handleChange = (e: React.ChangeEvent) => { + const val = e.target.value; + setDraft(val); + clearTimeout(saveTimer.current); + saveTimer.current = setTimeout(async () => { + setSaving(true); + await setNote(userId, val); + setSaving(false); + }, 800); + }; + + useEffect(() => () => clearTimeout(saveTimer.current), []); + + const charsLeft = USER_NOTE_MAX_LENGTH - draft.length; + + return ( + + + Private Note + + {saving ? 'Saving…' : charsLeft < 100 ? `${charsLeft} left` : ''} + + +