diff --git a/src/app/components/UserRoomProfileRenderer.tsx b/src/app/components/UserRoomProfileRenderer.tsx index ca7aa83..81188ea 100644 --- a/src/app/components/UserRoomProfileRenderer.tsx +++ b/src/app/components/UserRoomProfileRenderer.tsx @@ -34,7 +34,7 @@ function UserRoomProfileContextMenu({ state }: { state: UserRoomProfileState }) escapeDeactivates: stopPropagation, }} > - + diff --git a/src/app/components/presence/Presence.tsx b/src/app/components/presence/Presence.tsx index b5a7222..fde4439 100644 --- a/src/app/components/presence/Presence.tsx +++ b/src/app/components/presence/Presence.tsx @@ -67,6 +67,7 @@ type AvatarPresenceProps = { export const AvatarPresence = as<'div', AvatarPresenceProps>( ({ as: AsAvatarPresence, badge, variant = 'Surface', badgeBackgroundColor, children, ...props }, ref) => ( + {children} {badge && (
( {badge}
)} - {children}
) ); diff --git a/src/app/components/presence/styles.css.ts b/src/app/components/presence/styles.css.ts index 12ea7f1..50ec779 100644 --- a/src/app/components/presence/styles.css.ts +++ b/src/app/components/presence/styles.css.ts @@ -12,7 +12,7 @@ export const AvatarPresenceBadge = style({ bottom: 0, right: 0, transform: 'translate(25%, 25%)', - zIndex: 1, + zIndex: 10, display: 'flex', padding: config.borderWidth.B600, diff --git a/src/app/components/user-profile/ProfileCollectibleOverlays.tsx b/src/app/components/user-profile/ProfileCollectibleOverlays.tsx new file mode 100644 index 0000000..87f01aa --- /dev/null +++ b/src/app/components/user-profile/ProfileCollectibleOverlays.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { + pickStoredProfileEffectIntroUrl, + pickStoredProfileEffectLoopUrl, +} from '../../utils/collectibleAssets'; +import { ProfileEffectMedia } from './ProfileEffectMedia'; +import * as css from './styles.css'; + +type ProfileCollectibleOverlaysProps = { + assetUrls: Record; +}; + +export function ProfileCollectibleOverlays({ assetUrls }: ProfileCollectibleOverlaysProps) { + const introUrl = pickStoredProfileEffectIntroUrl(assetUrls); + const loopUrl = pickStoredProfileEffectLoopUrl(assetUrls); + + if (!introUrl && !loopUrl) return null; + + return ( + + ); +} diff --git a/src/app/components/user-profile/ProfileEffectMedia.tsx b/src/app/components/user-profile/ProfileEffectMedia.tsx new file mode 100644 index 0000000..e136294 --- /dev/null +++ b/src/app/components/user-profile/ProfileEffectMedia.tsx @@ -0,0 +1,77 @@ +import React, { useEffect, useState } from 'react'; +import { isCatalogVideoPreview } from '../../utils/collectibleAssets'; + +const DEFAULT_INTRO_DURATION_MS = 3000; + +type ProfileEffectMediaProps = { + introUrl?: string; + loopUrl?: string; + introDurationMs?: number; + className?: string; + restartToken?: number; +}; + +export function ProfileEffectMedia({ + introUrl, + loopUrl, + introDurationMs, + className, + restartToken = 0, +}: ProfileEffectMediaProps) { + const resolvedLoopUrl = loopUrl ?? introUrl; + const hasSequence = Boolean(introUrl && resolvedLoopUrl && introUrl !== resolvedLoopUrl); + const [phase, setPhase] = useState<'intro' | 'loop'>(hasSequence ? 'intro' : 'loop'); + const [playbackEpoch, setPlaybackEpoch] = useState(0); + + useEffect(() => { + setPhase(hasSequence ? 'intro' : 'loop'); + }, [introUrl, resolvedLoopUrl, hasSequence]); + + useEffect(() => { + setPhase(hasSequence ? 'intro' : 'loop'); + setPlaybackEpoch((epoch) => epoch + 1); + }, [restartToken, hasSequence]); + + const activeUrl = phase === 'intro' && introUrl ? introUrl : resolvedLoopUrl; + const shouldLoop = !hasSequence || phase === 'loop'; + + useEffect(() => { + if (!hasSequence || phase !== 'intro' || !introUrl) return undefined; + if (isCatalogVideoPreview(introUrl)) return undefined; + + const duration = introDurationMs ?? DEFAULT_INTRO_DURATION_MS; + const timer = window.setTimeout(() => setPhase('loop'), duration); + return () => window.clearTimeout(timer); + }, [hasSequence, phase, introUrl, introDurationMs, playbackEpoch]); + + if (!activeUrl) return null; + + if (isCatalogVideoPreview(activeUrl)) { + return ( +