import { Button, buttonVariants } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from "@/components/ui/sheet"; import useAvatars from "@/hooks/use-avatars"; import { useToast } from "@/hooks/use-toast"; import type { VideoView } from "@/hooks/use-video-record"; import { useVodLike } from "@/hooks/use-vod-like"; import { cn } from "@/lib/utils"; import { Link } from "@tanstack/react-router"; import { Check, Code2, Download, Ellipsis, ExternalLink, Heart, Link2, Send, Share2, } from "lucide-react"; import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { place } from "streamplace"; import { buildVodShareLinks, getAuthorHandle, shouldCollapseDescription, } from "./vod-watch"; type VodWatchHeaderProps = { video: VideoView; routeUser: string; tid: string; downloading: boolean; onDownload: () => void; }; export function VodWatchHeader({ video, routeUser, tid, downloading, onDownload, }: VodWatchHeaderProps) { const { t, i18n } = useTranslation("common"); const { show: showToast } = useToast(); const [descriptionExpanded, setDescriptionExpanded] = useState(false); const [copied, setCopied] = useState(null); const [failedAvatar, setFailedAvatar] = useState(null); const record = video.record as place.stream.video.Main; const dids = useMemo(() => [video.author.did], [video.author.did]); const profiles = useAvatars(dids); const profile = profiles[video.author.did]; const authorHandle = getAuthorHandle(video.author, routeUser); const authorRoute = video.author.handle || video.author.did; const avatar = profile?.avatar ?? video.author.avatar; const showAvatar = avatar && failedAvatar !== avatar; const description = record.description?.trim() ?? ""; const collapsible = shouldCollapseDescription(description); const createdAt = record.createdAt ? new Intl.DateTimeFormat(i18n.language, { dateStyle: "long" }).format( new Date(record.createdAt), ) : null; const viewCount = video.viewCounts?.count ?? 0; const title = record.title || t("untitled"); const nativeNavigator = ( typeof navigator === "undefined" ? {} : navigator ) as Navigator & { share?: (data?: ShareData) => Promise; }; const canNativeShare = typeof nativeNavigator.share === "function"; const shareLinks = buildVodShareLinks( typeof window === "undefined" ? "https://stream.place" : window.location.origin, routeUser, tid, ); const like = useVodLike({ subjectUri: video.uri, initialCount: video.likeCount ?? 0, }); const copy = async (value: string, key: string) => { try { await navigator.clipboard.writeText(value); setCopied(key); showToast(t("share-copied"), undefined, { variant: "success" }); window.setTimeout(() => setCopied(null), 1600); } catch { showToast(t("share-copy-failed"), undefined, { variant: "error" }); } }; const shareToBluesky = () => { const message = `${title} ${shareLinks.pageUrl}`; window.open( `https://bsky.app/intent/compose?text=${encodeURIComponent(message)}`, "_blank", "noopener,noreferrer", ); }; const nativeShare = async () => { if (!nativeNavigator.share) return; try { await nativeNavigator.share({ title, text: t("share-video-message", { title }), url: shareLinks.pageUrl, }); } catch (error) { if (error instanceof DOMException && error.name === "AbortError") return; showToast(t("share-failed"), undefined, { variant: "error" }); } }; const shareItems = ( <> {canNativeShare && ( {t("share-more-options")} )} {t("share-to-bluesky")} copy(shareLinks.pageUrl, "link")} > {copied === "link" ? t("share-copied") : t("share-copy-link")} ); return (

{title}

{t("views-count", { count: viewCount })} {createdAt && ( <> {createdAt} )}

{showAvatar ? ( setFailedAvatar(avatar)} /> ) : ( (authorHandle[0]?.toUpperCase() ?? "?") )}
{authorHandle}
{t("share-video")} {canNativeShare && ( {t("share-more-options")} )} {t("share-to-bluesky")} copy(shareLinks.pageUrl, "link")} > {copied === "link" ? : } {copied === "link" ? t("share-copied") : t("share-copy-link")}
} /> {t("share-video")} {title}
{shareItems}
{t("video-tools")} {t("download-video")} {t("video-embed")} copy(shareLinks.embedUrl, "embed-url")} > {copied === "embed-url" ? : } {copied === "embed-url" ? t("share-copied") : t("share-copy-embed-url")} copy(shareLinks.embedCode, "embed-code")} > {copied === "embed-code" ? : } {copied === "embed-code" ? t("share-copied") : t("share-copy-embed-code")}
{description && (

{description}

{collapsible && ( )}
)}
); } function ShareAction({ icon: Icon, onClick, children, }: { icon: typeof Share2; onClick: () => void; children: React.ReactNode; }) { return ( {children} } /> ); }