import React, { MouseEventHandler, ReactNode, useRef } from 'react'; import { toRem } from 'folds'; import { StreamMediaBubble, StreamMediaUsernameLabel, StreamMediaUsernameOverlay, } from './StreamMedia.css'; import { logMedia, useMediaMeasureDebug } from './streamMediaDebug'; const STREAM_MEDIA_MAX_DIM = 320; const STREAM_MEDIA_MIN_ASPECT = 3 / 4; const STREAM_MEDIA_MAX_ASPECT = 4 / 3; export type StreamMediaShellProps = { naturalW?: number; naturalH?: number; own: boolean; overlay?: ReactNode; senderId?: string; onUsernameClick?: MouseEventHandler; onUsernameContextMenu?: MouseEventHandler; children: ReactNode; }; function computeBoxStyle(naturalW?: number, naturalH?: number): React.CSSProperties { // Explicit pixel width + height — required so the inner content (folds // ImageContent / VideoContent — `RelativeBase` with `height: 100%`) gets // a definite parent height to resolve against. `max-width: 100%` from // StreamMediaBubble + the surrounding StreamBubble's mediaMode // `display: block; width: 100%` chain shrinks the bubble on narrow // viewports; the small aspect drift this introduces is masked by // `object-fit: cover` (image) / `contain` (video) on the inner element. const naturalAspect = naturalW && naturalH ? naturalW / naturalH : NaN; if ( !Number.isFinite(naturalAspect) || naturalAspect < STREAM_MEDIA_MIN_ASPECT || naturalAspect > STREAM_MEDIA_MAX_ASPECT ) { return { width: toRem(STREAM_MEDIA_MAX_DIM), height: toRem(STREAM_MEDIA_MAX_DIM) }; } if (naturalAspect >= 1) { const w = Math.min(STREAM_MEDIA_MAX_DIM, naturalW!); return { width: toRem(w), height: toRem(w / naturalAspect) }; } const h = Math.min(STREAM_MEDIA_MAX_DIM, naturalH!); return { width: toRem(h * naturalAspect), height: toRem(h) }; } // Shared chrome for image / video timeline bubbles: square-ish bubble with // asymmetric notch + 1px pseudo-frame, and the sender username overlaid // top-left as a text chip. Caller plugs in the actual media renderer as // `children`. // // Tab order: chip rendered BEFORE children so keyboard focus visits the // username (top-left visual) before the media tap target — matches reading // order. export function StreamMediaShell({ naturalW, naturalH, own, overlay, senderId, onUsernameClick, onUsernameContextMenu, children, }: StreamMediaShellProps) { const bubbleRef = useRef(null); const computedStyle = computeBoxStyle(naturalW, naturalH); logMedia('StreamMediaShell', { own, naturalW, naturalH, overlayPresent: !!overlay, senderId, computedStyle: { ...computedStyle }, }); useMediaMeasureDebug(bubbleRef, [computedStyle.width, computedStyle.height]); return (
{overlay && (
)} {children}
); }