Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/web/src/components/chat/MessagesTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,6 @@ describe("MessagesTimeline", () => {

expect(markup).toContain("<video");
expect(markup).toContain('aria-label="demo.mp4"');
expect(markup).toContain('controls=""');
expect(markup).not.toContain("Expand demo.mp4");
});

Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,10 @@ function UserVideoAttachment({ file }: { readonly file: ChatFileAttachment }) {
}
label={file.name}
preload="visible"
onOpen={() => {
const preview = buildAttachmentVideoPreview(ctx.activeThreadEnvironmentId, file);
if (preview) ctx.onImageExpand(preview);
}}
className="block aspect-[4/3] w-full"
videoClassName="aspect-auto size-full rounded-lg border border-border/80"
stateClassName="aspect-auto min-h-full rounded-lg border border-border/80 bg-black text-white"
Expand Down
35 changes: 28 additions & 7 deletions apps/web/src/components/media/MediaVideoPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RotateCwIcon, TriangleAlertIcon } from "lucide-react";
import { PlayIcon, RotateCwIcon, TriangleAlertIcon } from "lucide-react";
import { useCallback, useEffect, useRef, useState, type CSSProperties } from "react";

import { cn } from "../../lib/utils";
Expand All @@ -15,6 +15,8 @@ interface MediaVideoPlayerProps {
readonly revision?: string | null | undefined;
readonly preload?: "visible" | "metadata" | undefined;
readonly autoPlay?: boolean | undefined;
/** Presents a still thumbnail whose full surface opens the video in a viewer. */
readonly onOpen?: (() => void) | undefined;
readonly className?: string | undefined;
readonly videoClassName?: string | undefined;
/** Styles the loading and failure panels, which otherwise assume an inline light surface. */
Expand All @@ -34,6 +36,7 @@ export function MediaVideoPlayer({
revision = null,
preload = "visible",
autoPlay = false,
onOpen,
className,
videoClassName,
stateClassName,
Expand Down Expand Up @@ -131,7 +134,7 @@ export function MediaVideoPlayer({
style={style}
data-markdown-copy={copyMarkdown}
>
{failed ? (
{failed && !onOpen ? (
<span
role="alert"
className={cn(
Expand Down Expand Up @@ -160,17 +163,23 @@ export function MediaVideoPlayer({
<OpenMediaLink originalUrl={originalUrl} src={latestSrc ?? src} fileName={label} />
</span>
</span>
) : src !== null ? (
) : src !== null && !failed ? (
<video
key={loadAttempt}
ref={videoRef}
src={src}
aria-label={label || "Video preview"}
autoPlay={autoPlay}
controls
aria-hidden={onOpen ? true : undefined}
autoPlay={onOpen ? false : autoPlay}
controls={!onOpen}
muted={onOpen ? true : undefined}
playsInline
preload={preload === "metadata" || preloadedSrc === src ? "metadata" : "none"}
className={cn("aspect-video max-h-full w-full bg-black object-contain", videoClassName)}
className={cn(
"aspect-video max-h-full w-full bg-black object-contain",
onOpen && "pointer-events-none",
videoClassName,
)}
style={style}
onLoadedMetadata={(event) => prepareVideoFirstFrame(event.currentTarget)}
onPlay={() => setPlaybackSource({ src, revision: sourceRevision })}
Expand All @@ -184,11 +193,23 @@ export function MediaVideoPlayer({
) : (
<span
role="status"
aria-label="Loading video"
aria-label={failed ? "Video preview unavailable" : "Loading video"}
className={cn("block aspect-video w-full rounded-lg bg-muted/60", stateClassName)}
style={style}
/>
)}
{onOpen ? (
<button
type="button"
aria-label={label ? `Play ${label}` : "Play video"}
onClick={onOpen}
className="absolute inset-0 flex cursor-pointer items-center justify-center rounded-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
>
<span className="flex size-8 items-center justify-center rounded-full bg-black/50 text-white">
<PlayIcon aria-hidden className="size-4 fill-current" />
</span>
</button>
) : null}
</span>
);
return actionsSource ? <MediaActions source={actionsSource}>{player}</MediaActions> : player;
Expand Down
Loading