fix(lotus): decoration ring sized around the avatar, hidden on error / reduced motion, CDN-pinned, survives remount

- .lotusDecoration 50cqmin -> 62cqmin (cinny's inset ratio); onError
  hides a broken image (#4).
- display:none under prefers-reduced-motion, matching the host (#19).
- safeImageUrl only accepts ALLOWED_DECORATION_ORIGINS (the decorations
  CDN) plus blob: (#28).
- Roster is no longer wiped on last teardown; the handler sends
  io.lotus.request_state on (re)registration so the host can re-push
  decorations and the pin (#17 — host half in cinny).

Fixes #4
Fixes #19
Fixes #28
Fixes #17

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
Lotus CI
2026-09-13 01:22:20 -04:00
co-authored by Claude Opus 5
parent e5d5f13923
commit bb639bb92d
4 changed files with 189 additions and 11 deletions
+13
View File
@@ -113,6 +113,15 @@ export const MediaView: FC<Props> = ({
}) => {
const { t } = useTranslation();
const decoration = useLotusDecoration(userId);
// [lotus #4] Track the URL of a decoration that failed to load (e.g. a 404
// from the CDN, reachable given the unvalidated slug the host builds it
// from) so we can hide the broken-image box instead of leaving it over the
// avatar. Comparing against the current `decoration` (rather than a bare
// boolean) means a new/changed decoration URL automatically gets a fresh
// chance to load.
const [erroredDecoration, setErroredDecoration] = useState<
string | undefined
>(undefined);
const [handRaiseTimerVisible] = useSetting(showHandRaisedTimer);
const [showConnectionStats] = useSetting(showConnectionStatsSetting);
@@ -188,15 +197,19 @@ export const MediaView: FC<Props> = ({
style={{ display: video && videoEnabled ? "none" : "initial" }}
/>
{decoration &&
decoration !== erroredDecoration &&
!(video && videoEnabled) && (
// [lotus #6] Profile decoration overlay, shown only when the avatar
// is visible (i.e. not when live video is showing). Pushed by the
// host via io.lotus.decorations; undefined unless opted in.
// [lotus #4] Hidden entirely under prefers-reduced-motion (CSS) and
// on a load error (onError), matching cinny's own guards.
<img
className={styles.lotusDecoration}
src={decoration}
alt=""
aria-hidden
onError={() => setErroredDecoration(decoration)}
/>
)}
{video?.publication !== undefined && (