diff --git a/.gitignore b/.gitignore index 13c74945b..6f39e04e9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ devAssets .DS_Store .ideapackage-lock.json +public/decorations/ diff --git a/LOTUS_FEATURES.md b/LOTUS_FEATURES.md index e44870473..126bfbf94 100644 --- a/LOTUS_FEATURES.md +++ b/LOTUS_FEATURES.md @@ -11,20 +11,21 @@ Last updated: June 2026. 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. [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) +5. [Avatar Decorations (P5-13/P5-14)](#avatar-decorations-p5-13p5-14) +6. [Glassmorphism Sidebar (P5-3)](#glassmorphism-sidebar-p5-3) +7. [Night Light / Blue Light Filter (P5-5)](#night-light--blue-light-filter-p5-5) +8. [Voice / Video Call Improvements](#voice--video-call-improvements) +9. [Per-Message Read Receipts](#per-message-read-receipts) +10. [Delivery Status Indicators](#delivery-status-indicators) +11. [Messaging Enhancements](#messaging-enhancements) +12. [Presence](#presence) +13. [UX & Composer](#ux--composer) +14. [Room Customization](#room-customization) +15. [Moderation](#moderation) +16. [Notifications](#notifications) +17. [Server Integration](#server-integration) +18. [Infrastructure](#infrastructure) +19. [Key Custom Files](#key-custom-files) --- @@ -200,6 +201,75 @@ Decorative CSS-only overlays that activate automatically on holidays and events. --- +## Avatar Decorations (P5-13/P5-14) + +Animated APNG overlay frames that float around user avatars, inspired by Discord's Avatar Decorations feature. Each decoration extends 8px beyond the avatar border on all sides, with a transparent center hole that reveals the avatar beneath. Other Lotus Chat users see your selected decoration in real time — stored in the Matrix profile via MSC4133. + +### Decoration Library + +99 hand-curated, original-IP decorations (no licensed character artwork) organized into 9 categories: + +| Category | Count | Highlights | +|---|---|---| +| Gaming | 13 | Slither 'n Snack, Joystick, Space Invaders, Gaming Headsets | +| Cyber | 9 | Cybernetic, Glitch, Digital Sunrise, Futuristic UI (3 colors) | +| Space | 8 | Black Hole, Constellations, Solar Orbit, Aurora | +| Fantasy | 22 | Kitsune, Phoenix, Glowing Runes, D&D dice, Crystal Balls | +| Elements | 7 | Fire, Water, Air, Earth, Lightning, Ki Energy | +| Japanese | 6 | Kabuto, Oni Mask, Sakura Warrior, Straw Hat | +| Nature | 12 | **Lotus Flower**, Koi Pond, Sakura, Fall Leaves, Fireflies | +| Spooky | 13 | Candlelight, Witch Hat, Ghosts, Jack-o'-Lantern | +| Cozy | 11 | Cozy Cat, Fox Hat (3 colors), Cat Ears, Frog Hat | + +All decoration files are 256×256 APNGs. They animate natively in all modern browsers via `` elements. + +### Architecture + +**Profile storage — MSC4133:** +Decoration preference is stored in the public Matrix profile field `io.lotus.avatar_decoration` (a slug string, e.g. `lotus_flower`). Any Lotus Chat user viewing your profile sees your current decoration. + +**CDN:** +Files are self-hosted on the Lotus Nextcloud instance. Direct access: `https://drive.lotusguild.org/public.php/dav/files/{token}/cinny-decorations/{slug}.png`. `` elements load cross-origin freely — no CORS headers needed. + +**Module-level cache with in-flight deduplication:** +`useAvatarDecoration(userId)` fetches the profile field once per user per session. A `Map` cache prevents redundant requests; a second `pending` waiters map ensures multiple components requesting the same userId simultaneously share one HTTP request rather than firing duplicates. + +**Wrapping pattern:** +`AvatarDecoration` renders a `position: relative; display: inline-flex` wrapper div. The decoration `` is `position: absolute` with `top/left/right/bottom: -8px`, extending equally on all sides while the `z-index: 10` keeps it above the avatar. `onError` hides the image if the CDN file is absent. This wrapper sits outside `PresenceRingAvatar` so the presence ring and decoration layer are fully independent. + +### Placement — Where Decorations Render + +| Location | File | +|---|---| +| Message timeline | `src/app/features/room/message/Message.tsx` | +| Members drawer | `src/app/features/room/MembersDrawer.tsx` | +| `@mention` autocomplete | `src/app/components/editor/autocomplete/UserMentionAutocomplete.tsx` | +| Inbox / notifications | `src/app/pages/client/inbox/Notifications.tsx` | + +### Settings — Decoration Picker + +**Settings → Account → Avatar Decoration** shows a scrollable grid of all decorations, grouped by category. Each cell is a 52×52px button with a live preview of the APNG. The currently selected decoration gets a 2px cyan border. "No Decoration" clears the field. Changes are saved only when the "Save" button is clicked (visible only when a change is pending). After save, `invalidateDecorationCache(userId)` forces other components to re-fetch. + +### Catalog Sync Script + +After deleting decoration files from the Nextcloud share, run: + +```bash +npm run sync:decorations +``` + +The script (`scripts/syncDecorations.mjs`) sends HTTP HEAD requests to the CDN URL for every slug in `avatarDecorations.ts` and automatically removes entries for files that returned 404. Empty categories are pruned automatically. Review with `git diff`. + +### Files + +- `src/app/features/lotus/avatarDecorations.ts` — full catalog (`DECORATION_CATEGORIES`, `ALL_DECORATIONS`, `decorationUrl()`, `DECORATION_CDN`) +- `src/app/hooks/useAvatarDecoration.ts` — profile fetch, module-level cache, `invalidateDecorationCache()` +- `src/app/components/avatar-decoration/AvatarDecoration.tsx` — wrapper component with APNG overlay +- `src/app/features/settings/account/ProfileDecoration.tsx` — settings UI (picker grid, save button) +- `scripts/syncDecorations.mjs` — CDN sync script to prune deleted decorations from the catalog + +--- + ## Glassmorphism Sidebar (P5-3) An optional frosted-glass sidebar style toggled in **Settings → Appearance**. @@ -969,3 +1039,8 @@ The `encUrlPreview` setting defaults to `true` rather than `false`. A security a | `src/app/hooks/useExtendedProfile.ts` | MSC4133 extended profile fields (`m.pronouns`, `m.tz`) read/write | | `src/app/hooks/useLocalTime.ts` | Derives current local time from `m.tz` profile field, updates every 60s | | `src/app/components/url-preview/UrlPreviewCard.tsx` | 13 domain-specific URL preview layouts plus generic fallback with favicon | +| `src/app/features/lotus/avatarDecorations.ts` | Avatar decoration catalog, CDN URL, `decorationUrl()` helper | +| `src/app/hooks/useAvatarDecoration.ts` | Profile field fetch with module-level cache and in-flight deduplication | +| `src/app/components/avatar-decoration/AvatarDecoration.tsx` | APNG overlay wrapper rendered around avatars in timeline, members drawer, autocomplete | +| `src/app/features/settings/account/ProfileDecoration.tsx` | Settings decoration picker — scrollable grid, category headers, save button | +| `scripts/syncDecorations.mjs` | CDN HEAD-check sync script: removes catalog entries for deleted Nextcloud files | diff --git a/LOTUS_TODO.md b/LOTUS_TODO.md index 92a18416d..f6d730b61 100644 --- a/LOTUS_TODO.md +++ b/LOTUS_TODO.md @@ -260,7 +260,7 @@ Themes: --- -### [ ] P5-12 · Seasonal / Event Themes +### [x] P5-12 · Seasonal / Event Themes **What:** Automatic + manually toggleable seasonal overlays with CSS particle effects and accent color variants: @@ -274,7 +274,7 @@ Themes: --- -### [ ] P5-13 · Avatar Frame / Border Decorations +### [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). @@ -282,7 +282,7 @@ Themes: --- -### [ ] P5-14 · Animated Avatar Overlay Decorations (Discord-style) +### [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. @@ -355,7 +355,7 @@ Themes: --- -### [ ] P5-34 · User-to-User Private Notes +### [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`. diff --git a/README.md b/README.md index c6fa3a11f..40156c90b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ The Lotus Chat logo (`lotus_chat.png`) is a derivative work based on the origina - 20+ static chat background patterns - 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 +- Avatar decorations — 99 animated APNG overlays (Gaming, Cyber, Space, Fantasy, Nature, Spooky, Cozy, and more) that frame your avatar across the timeline, members list, and @mention autocomplete; visible to all Lotus Chat users; select in Settings → Account → Avatar Decoration - 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 diff --git a/package.json b/package.json index 7896c320f..e2c1af6cf 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "typecheck": "tsc --noEmit", "prepare": "husky", "commit": "git-cz", - "postinstall": "node scripts/patch-folds.mjs" + "postinstall": "node scripts/patch-folds.mjs", + "sync:decorations": "node scripts/syncDecorations.mjs" }, "lint-staged": { "*.{ts,tsx,js,jsx}": "eslint", diff --git a/scripts/syncDecorations.mjs b/scripts/syncDecorations.mjs new file mode 100644 index 000000000..78009e0ea --- /dev/null +++ b/scripts/syncDecorations.mjs @@ -0,0 +1,89 @@ +#!/usr/bin/env node +/** + * Syncs avatarDecorations.ts with what's actually available on the Nextcloud CDN. + * + * Usage: + * npm run sync:decorations + * + * Workflow after deleting files from Nextcloud: + * 1. Delete decoration files from your Nextcloud share. + * 2. Run: npm run sync:decorations + * 3. It probes each catalog slug via HTTP HEAD and removes entries + * whose files returned 404. Empty categories are dropped automatically. + * 4. Commit the updated avatarDecorations.ts. + */ + +import { readFileSync, writeFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, '..'); +const catalogPath = join(root, 'src', 'app', 'features', 'lotus', 'avatarDecorations.ts'); + +const CDN = + 'https://drive.lotusguild.org/public.php/dav/files/bHswJ9pNKp2t26N/cinny-decorations'; + +// Extract all slugs from the catalog file +const catalog = readFileSync(catalogPath, 'utf8'); +const slugMatches = [...catalog.matchAll(/slug: '([^']+)'/g)].map((m) => m[1]); + +if (slugMatches.length === 0) { + console.error('No slugs found in catalog — check the file path.'); + process.exit(1); +} + +console.log(`Checking ${slugMatches.length} decorations against ${CDN} …`); +console.log('(This makes one HEAD request per decoration)\n'); + +// Probe all slugs in parallel batches of 16 +async function headCheck(slug) { + try { + const res = await fetch(`${CDN}/${slug}.png`, { method: 'HEAD' }); + return { slug, ok: res.ok, status: res.status }; + } catch { + return { slug, ok: false, status: 0 }; + } +} + +const BATCH = 16; +const results = []; +for (let i = 0; i < slugMatches.length; i += BATCH) { + const batch = slugMatches.slice(i, i + BATCH); + const batchResults = await Promise.all(batch.map(headCheck)); + results.push(...batchResults); +} + +const missing = results.filter((r) => !r.ok); +const found = results.filter((r) => r.ok); + +if (missing.length === 0) { + console.log(`All ${found.length} decorations are available — catalog is up to date.`); + process.exit(0); +} + +console.log(`Found: ${found.length} Missing: ${missing.length}\n`); +missing.forEach((r) => + console.log(` Removing (HTTP ${r.status}): ${r.slug}`), +); + +const missingSet = new Set(missing.map((r) => r.slug)); + +// Remove individual entries for missing slugs +let updated = catalog.replace( + /^[ \t]*\{ slug: '([^']+)', name: .+\},?\r?\n/gm, + (match, slug) => (missingSet.has(slug) ? '' : match), +); + +// Drop category blocks that now have an empty decorations array +updated = updated.replace( + / \{\n id: '[^']+',\n label: '[^']+',\n decorations: \[\n?[ \t]*\],?\n \},?\n/g, + '', +); + +// Clean up stray blank lines +updated = updated.replace(/\n{3,}/g, '\n\n'); + +writeFileSync(catalogPath, updated, 'utf8'); +console.log(`\nDone. Removed ${missing.length} entr${missing.length === 1 ? 'y' : 'ies'} from the catalog.`); +console.log('Review with: git diff src/app/features/lotus/avatarDecorations.ts'); diff --git a/src/app/components/avatar-decoration/AvatarDecoration.tsx b/src/app/components/avatar-decoration/AvatarDecoration.tsx index 9ffcd0c10..ea32df1ed 100644 --- a/src/app/components/avatar-decoration/AvatarDecoration.tsx +++ b/src/app/components/avatar-decoration/AvatarDecoration.tsx @@ -46,6 +46,9 @@ export function AvatarDecoration({ userId, children }: AvatarDecorationProps) { aria-hidden="true" loading="lazy" decoding="async" + onError={(e) => { + (e.currentTarget as HTMLImageElement).style.display = 'none'; + }} /> ); diff --git a/src/app/features/lotus/avatarDecorations.ts b/src/app/features/lotus/avatarDecorations.ts index 8007a413b..59be0198c 100644 --- a/src/app/features/lotus/avatarDecorations.ts +++ b/src/app/features/lotus/avatarDecorations.ts @@ -1,4 +1,5 @@ -export const DECORATION_CDN = 'https://img.avatardecoration.com/decorations'; +export const DECORATION_CDN = + 'https://drive.lotusguild.org/public.php/dav/files/bHswJ9pNKp2t26N/cinny-decorations'; export type AvatarDecoration = { slug: string;