feat: support mobile video previews
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
} from "react";
|
||||
import type { Attachment } from "../../types/post";
|
||||
import { AttachmentDownloadPill } from "./AttachmentDownloadPill";
|
||||
import { useVideoPreviewSource } from "./hooks/useVideoPreviewSource";
|
||||
import { useVideoPlayer } from "./overlays/VideoPlayer";
|
||||
|
||||
function pad2(n: number): string {
|
||||
@@ -127,6 +128,7 @@ export function MessageInlineVideo({
|
||||
const [snapProgress, setSnapProgress] = useState(false);
|
||||
|
||||
const t = TOKENS[size];
|
||||
const videoSrc = useVideoPreviewSource(attachment);
|
||||
|
||||
useEffect(() => {
|
||||
const v = videoRef.current;
|
||||
@@ -270,7 +272,7 @@ export function MessageInlineVideo({
|
||||
<>
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={attachment.url}
|
||||
src={videoSrc}
|
||||
poster={attachment.posterUrl}
|
||||
playsInline
|
||||
autoPlay={autoPlay}
|
||||
|
||||
@@ -6,12 +6,20 @@ import { useI18n } from "../../../i18n";
|
||||
import type { Attachment, Post } from "../../../types/post";
|
||||
import { AttachmentDownloadPill } from "../AttachmentDownloadPill";
|
||||
import { MessageInlineVideo } from "../MessageInlineVideo";
|
||||
import {
|
||||
useShouldUseMobilePreview,
|
||||
useVideoPreviewSource,
|
||||
} from "../hooks/useVideoPreviewSource";
|
||||
import { useVideoPlayer } from "../overlays/VideoPlayer";
|
||||
import { autolink } from "../utils/autolink";
|
||||
import { CollapsibleText } from "../CollapsibleText";
|
||||
import { downloadAttachment } from "../utils/downloadFile";
|
||||
import { formatBytes } from "../utils/formatBytes";
|
||||
import { postDisplayText } from "../utils/postText";
|
||||
import {
|
||||
videoMetadataPreviewSource,
|
||||
videoPreviewSource,
|
||||
} from "../utils/videoPreviewSource";
|
||||
import { useToast } from "../../Toast";
|
||||
|
||||
const MAX_VISIBLE = 4;
|
||||
@@ -59,9 +67,8 @@ function VideoAttachmentCard({
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const posterUrl = attachment.posterUrl ?? attachment.thumbnailUrl;
|
||||
const duration = formatDuration(attachment.durationSec);
|
||||
const previewVideoUrl = attachment.url.includes("#")
|
||||
? attachment.url
|
||||
: `${attachment.url}#t=0.1`;
|
||||
const videoSrc = useVideoPreviewSource(attachment);
|
||||
const previewVideoUrl = videoMetadataPreviewSource(videoSrc);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -208,6 +215,8 @@ function VideoListDialog({
|
||||
onClose: () => void;
|
||||
onPick: (attachment: Attachment) => void;
|
||||
}) {
|
||||
const useMobilePreview = useShouldUseMobilePreview();
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") onClose();
|
||||
@@ -242,9 +251,9 @@ function VideoListDialog({
|
||||
<div className="max-h-[70vh] overflow-y-auto p-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
{videos.map((video, index) => {
|
||||
const thumb = video.posterUrl ?? video.thumbnailUrl;
|
||||
const previewVideoUrl = video.url.includes("#")
|
||||
? video.url
|
||||
: `${video.url}#t=0.1`;
|
||||
const previewVideoUrl = videoMetadataPreviewSource(
|
||||
videoPreviewSource(video, useMobilePreview),
|
||||
);
|
||||
const duration = formatDuration(video.durationSec);
|
||||
return (
|
||||
<div
|
||||
|
||||
34
src/components/messageStream/hooks/useVideoPreviewSource.ts
Normal file
34
src/components/messageStream/hooks/useVideoPreviewSource.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Attachment } from "../../../types/post";
|
||||
import {
|
||||
mobilePreviewMediaQuery,
|
||||
videoPreviewSource,
|
||||
} from "../utils/videoPreviewSource";
|
||||
|
||||
function getMatchesMobilePreview(): boolean {
|
||||
if (typeof window === "undefined" || !window.matchMedia) return false;
|
||||
return window.matchMedia(mobilePreviewMediaQuery).matches;
|
||||
}
|
||||
|
||||
export function useShouldUseMobilePreview(): boolean {
|
||||
const [useMobilePreview, setUseMobilePreview] = useState(
|
||||
getMatchesMobilePreview,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined" || !window.matchMedia) return;
|
||||
|
||||
const media = window.matchMedia(mobilePreviewMediaQuery);
|
||||
const update = () => setUseMobilePreview(media.matches);
|
||||
update();
|
||||
|
||||
media.addEventListener("change", update);
|
||||
return () => media.removeEventListener("change", update);
|
||||
}, []);
|
||||
|
||||
return useMobilePreview;
|
||||
}
|
||||
|
||||
export function useVideoPreviewSource(attachment: Attachment): string {
|
||||
return videoPreviewSource(attachment, useShouldUseMobilePreview());
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { Attachment } from "../../../types/post";
|
||||
import {
|
||||
videoMetadataPreviewSource,
|
||||
videoPreviewSource,
|
||||
} from "./videoPreviewSource";
|
||||
|
||||
const attachment: Attachment = {
|
||||
id: "att-1",
|
||||
kind: "video",
|
||||
url: "/uploads/desktop-preview.mp4",
|
||||
mobilePreviewUrl: "/uploads/mobile-540p-preview.mp4",
|
||||
mime: "video/mp4",
|
||||
filename: "original.mp4",
|
||||
sizeBytes: 1024,
|
||||
};
|
||||
|
||||
describe("videoPreviewSource", () => {
|
||||
it("uses the desktop preview by default", () => {
|
||||
expect(videoPreviewSource(attachment, false)).toBe(
|
||||
"/uploads/desktop-preview.mp4",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses mobilePreviewUrl only when mobile preview is active", () => {
|
||||
expect(videoPreviewSource(attachment, true)).toBe(
|
||||
"/uploads/mobile-540p-preview.mp4",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the desktop preview when mobilePreviewUrl is absent", () => {
|
||||
expect(
|
||||
videoPreviewSource({ ...attachment, mobilePreviewUrl: undefined }, true),
|
||||
).toBe("/uploads/desktop-preview.mp4");
|
||||
});
|
||||
|
||||
it("adds a metadata seek fragment only when the URL has no fragment", () => {
|
||||
expect(videoMetadataPreviewSource("/uploads/video.mp4")).toBe(
|
||||
"/uploads/video.mp4#t=0.1",
|
||||
);
|
||||
expect(videoMetadataPreviewSource("/uploads/video.mp4#t=2")).toBe(
|
||||
"/uploads/video.mp4#t=2",
|
||||
);
|
||||
});
|
||||
});
|
||||
17
src/components/messageStream/utils/videoPreviewSource.ts
Normal file
17
src/components/messageStream/utils/videoPreviewSource.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Attachment } from "../../../types/post";
|
||||
|
||||
export const mobilePreviewMediaQuery = "(max-width: 760px)";
|
||||
|
||||
export function videoPreviewSource(
|
||||
attachment: Attachment,
|
||||
useMobilePreview: boolean,
|
||||
): string {
|
||||
if (useMobilePreview && attachment.mobilePreviewUrl) {
|
||||
return attachment.mobilePreviewUrl;
|
||||
}
|
||||
return attachment.url;
|
||||
}
|
||||
|
||||
export function videoMetadataPreviewSource(url: string): string {
|
||||
return url.includes("#") ? url : `${url}#t=0.1`;
|
||||
}
|
||||
Reference in New Issue
Block a user