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
+16 -4
View File
@@ -120,8 +120,11 @@ Please see LICENSE in the repository root for full details.
opacity: 50%;
}
/* [lotus #6] Profile decoration overlaid on the tile avatar. Shares the
avatar's centred box and size so frame-style decorations sit around it. */
/* [lotus #4] Profile decoration overlaid on the tile avatar. Sized larger than
the avatar's box (62cqmin vs. the avatar's 50cqmin — matching cinny's own
~50px avatar + 8px outward inset ratio, `AvatarDecoration.tsx`'s
DEFAULT_INSET) so frame-style decoration artwork bleeds outside the avatar
circle and surrounds it, instead of overlapping/clipping it 1:1. */
.lotusDecoration {
position: absolute;
top: 50%;
@@ -131,10 +134,19 @@ avatar's centred box and size so frame-style decorations sit around it. */
object-fit: contain;
}
/* [lotus #19] Decorations are animated APNGs with no static asset to freeze
to; hide them under prefers-reduced-motion, matching cinny's own
AvatarDecoration guard and the host's behaviour of rendering just the avatar. */
@media (prefers-reduced-motion: reduce) {
.lotusDecoration {
display: none;
}
}
@container mediaView (width > 0) {
.lotusDecoration {
inline-size: 50cqmin;
block-size: 50cqmin;
inline-size: 62cqmin;
block-size: 62cqmin;
}
}
+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 && (