feat: avatar decorations follow-up — Nextcloud CDN, sync script, docs
- Point DECORATION_CDN at Lotus Nextcloud WebDAV share instead of external avatardecoration.com; all 99 APNG files are self-hosted and served via direct DAV URL (no CORS issue for <img> elements) - Add onError handler to AvatarDecoration.tsx to silently hide the overlay if a file is missing or the CDN is unreachable - Rewrite scripts/syncDecorations.mjs: now sends HTTP HEAD requests to the live Nextcloud CDN (batches of 16 in parallel) and removes catalog entries for files that return non-2xx; empty categories are pruned automatically. Workflow: delete files from Nextcloud → run `npm run sync:decorations` → commit the updated avatarDecorations.ts. No local files needed. - Add public/decorations/ to .gitignore; delete the 85 MB local APNG cache that was downloaded during development (files live on Nextcloud now) - Add sync:decorations script to package.json - Update LOTUS_FEATURES.md, LOTUS_TODO.md (P5-13 + P5-14 ✓), README.md with avatar decoration documentation and catalog sync workflow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,3 +5,4 @@ devAssets
|
|||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.ideapackage-lock.json
|
.ideapackage-lock.json
|
||||||
|
public/decorations/
|
||||||
|
|||||||
+89
-14
@@ -11,20 +11,21 @@ Last updated: June 2026.
|
|||||||
2. [LotusGuild Terminal Design System (TDS) v1.2](#lotusguild-terminal-design-system-tds-v12)
|
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)
|
3. [Animated Chat Backgrounds (P5-4)](#animated-chat-backgrounds-p5-4)
|
||||||
4. [Seasonal Theme Overlays (P5-12)](#seasonal-theme-overlays-p5-12)
|
4. [Seasonal Theme Overlays (P5-12)](#seasonal-theme-overlays-p5-12)
|
||||||
5. [Glassmorphism Sidebar (P5-3)](#glassmorphism-sidebar-p5-3)
|
5. [Avatar Decorations (P5-13/P5-14)](#avatar-decorations-p5-13p5-14)
|
||||||
6. [Night Light / Blue Light Filter (P5-5)](#night-light--blue-light-filter-p5-5)
|
6. [Glassmorphism Sidebar (P5-3)](#glassmorphism-sidebar-p5-3)
|
||||||
7. [Voice / Video Call Improvements](#voice--video-call-improvements)
|
7. [Night Light / Blue Light Filter (P5-5)](#night-light--blue-light-filter-p5-5)
|
||||||
8. [Per-Message Read Receipts](#per-message-read-receipts)
|
8. [Voice / Video Call Improvements](#voice--video-call-improvements)
|
||||||
9. [Delivery Status Indicators](#delivery-status-indicators)
|
9. [Per-Message Read Receipts](#per-message-read-receipts)
|
||||||
10. [Messaging Enhancements](#messaging-enhancements)
|
10. [Delivery Status Indicators](#delivery-status-indicators)
|
||||||
11. [Presence](#presence)
|
11. [Messaging Enhancements](#messaging-enhancements)
|
||||||
12. [UX & Composer](#ux--composer)
|
12. [Presence](#presence)
|
||||||
13. [Room Customization](#room-customization)
|
13. [UX & Composer](#ux--composer)
|
||||||
14. [Moderation](#moderation)
|
14. [Room Customization](#room-customization)
|
||||||
15. [Notifications](#notifications)
|
15. [Moderation](#moderation)
|
||||||
16. [Server Integration](#server-integration)
|
16. [Notifications](#notifications)
|
||||||
17. [Infrastructure](#infrastructure)
|
17. [Server Integration](#server-integration)
|
||||||
18. [Key Custom Files](#key-custom-files)
|
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 `<img>` 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`. `<img>` 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<userId, slug|null>` 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 `<img>` 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)
|
## Glassmorphism Sidebar (P5-3)
|
||||||
|
|
||||||
An optional frosted-glass sidebar style toggled in **Settings → Appearance**.
|
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/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/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/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 |
|
||||||
|
|||||||
+4
-4
@@ -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:
|
**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.
|
**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).
|
**[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.
|
**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.
|
**[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.
|
**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`.
|
**Matrix Tech:** Store in `io.lotus.user_notes` account data. Must be keyed by `userId`.
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ The Lotus Chat logo (`lotus_chat.png`) is a derivative work based on the origina
|
|||||||
- 20+ static chat background patterns
|
- 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)
|
- 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
|
- 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
|
- Toggle to pause background animations
|
||||||
- Glassmorphism sidebar — frosted glass effect that lets the background show through
|
- Glassmorphism sidebar — frosted glass effect that lets the background show through
|
||||||
- Night Light / blue light filter with an adjustable intensity slider
|
- Night Light / blue light filter with an adjustable intensity slider
|
||||||
|
|||||||
+2
-1
@@ -18,7 +18,8 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"commit": "git-cz",
|
"commit": "git-cz",
|
||||||
"postinstall": "node scripts/patch-folds.mjs"
|
"postinstall": "node scripts/patch-folds.mjs",
|
||||||
|
"sync:decorations": "node scripts/syncDecorations.mjs"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx,js,jsx}": "eslint",
|
"*.{ts,tsx,js,jsx}": "eslint",
|
||||||
|
|||||||
@@ -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');
|
||||||
@@ -46,6 +46,9 @@ export function AvatarDecoration({ userId, children }: AvatarDecorationProps) {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
|
onError={(e) => {
|
||||||
|
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 = {
|
export type AvatarDecoration = {
|
||||||
slug: string;
|
slug: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user