Private
Public Access
Play video attachments inline, with an expand option (#65)
A video file previously rendered as a generic downloadable file card, same as any other attachment. The file-serve endpoint forces Content-Disposition: attachment for every upload as an XSS mitigation (a same-origin-served .html/.svg executing script), which also meant a <video> tag pointed at it couldn't play -- the browser would just try to download it. Carve out a strict, server-side allowlist (video/mp4, video/webm, video/ogg -- deliberately not "every video/* type") that skips the forced download, the same reasoning MessageImage's own endpoint already relies on: these are content types a browser only ever interprets as media, never as something that could execute script. Anything else, including other video formats like .mov, still forces a download exactly as before. On the frontend, a video attachment with one of those content types renders as an inline <video controls> instead of the generic file card, with a hover-revealed expand button that calls the browser's native Fullscreen API on the video element directly rather than building a second lightbox component. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,44 @@
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.message-video-wrap {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
max-width: min(320px, 100%);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.message-video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: 240px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--ds-border);
|
||||
background: var(--ds-void);
|
||||
}
|
||||
|
||||
.message-video-expand {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.message-video-wrap:hover .message-video-expand,
|
||||
.message-video-expand:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.message-file-attachment {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -60,6 +60,49 @@ function FileAttachmentCard({ file, roomId, onPreview }: FileAttachmentCardProps
|
||||
)
|
||||
}
|
||||
|
||||
// #65: kept in sync with backend/app/storage.py's INLINE_SAFE_VIDEO_
|
||||
// CONTENT_TYPES -- the server only ever serves these particular content
|
||||
// types without a forced download, so a <video> tag pointed at anything
|
||||
// else would just show a broken player instead of playing (or, worse,
|
||||
// trigger a download the moment the browser tries to fetch it).
|
||||
const PLAYABLE_VIDEO_CONTENT_TYPES = new Set(['video/mp4', 'video/webm', 'video/ogg'])
|
||||
|
||||
interface VideoAttachmentProps {
|
||||
file: MessageFileInfo
|
||||
roomId: string
|
||||
}
|
||||
|
||||
// Plays inline via the browser's own <video controls> (no custom overlay
|
||||
// needed for play/pause/volume/seek) -- the one thing it doesn't give a
|
||||
// small inline player is an obvious way to go bigger, so this adds an
|
||||
// explicit expand button on top calling the standard Fullscreen API
|
||||
// directly on the video element, rather than building a whole second
|
||||
// lightbox component just to re-embed the same <video>.
|
||||
function VideoAttachment({ file, roomId }: VideoAttachmentProps) {
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
return (
|
||||
<div className="message-video-wrap">
|
||||
<video ref={videoRef} src={getRoomFileUrl(roomId, file.id)} controls className="message-video" />
|
||||
<button
|
||||
type="button"
|
||||
className="message-video-expand"
|
||||
onClick={() => videoRef.current?.requestFullscreen()}
|
||||
aria-label="Expand video"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M7 3H3v4M13 3h4v4M3 13v4h4M17 13v4h-4"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface MessageListProps {
|
||||
roomId: string
|
||||
messages: (Message | ChatMessageEnvelope)[]
|
||||
@@ -215,7 +258,10 @@ export function MessageList({
|
||||
onClick={() => setLightboxSrc(getRoomImageUrl(roomId, msg.image_id!))}
|
||||
/>
|
||||
)}
|
||||
{msg.file && (
|
||||
{msg.file && PLAYABLE_VIDEO_CONTENT_TYPES.has(msg.file.content_type) && (
|
||||
<VideoAttachment file={msg.file} roomId={roomId} />
|
||||
)}
|
||||
{msg.file && !PLAYABLE_VIDEO_CONTENT_TYPES.has(msg.file.content_type) && (
|
||||
<FileAttachmentCard
|
||||
file={msg.file}
|
||||
roomId={roomId}
|
||||
|
||||
Reference in New Issue
Block a user