import { useStore } from "@/lib/store"; import { useStreamplaceUrl } from "@/lib/store/hooks"; import { cn } from "@/lib/utils"; import type { LivestreamStore } from "@streamplace/core"; import { Check, ChevronRight, ClipboardCopy, Plus, Share2 } from "lucide-react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { place } from "streamplace"; import { useStore as useLivestreamStore } from "zustand"; import { useShallow } from "zustand/react/shallow"; import type { Liveness } from "../../hooks/use-liveness-state"; import { useSession } from "../../lib/session"; import { Button, buttonVariants } from "../ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, } from "../ui/dropdown-menu"; import { StreamAvatar } from "./stream-avatar"; const ACTIVITY_I18N_KEYS: Record = { events: "activity-events", just_chatting: "activity-just-chatting", music: "activity-music", art: "activity-art", software_dev: "activity-software-dev", cooking: "activity-cooking", miniatures: "activity-miniatures", makers_crafting: "activity-makers-crafting", fitness: "activity-fitness", sports: "activity-sports", }; export function activityLabel( activity: | place.stream.defs.ActivityGame | place.stream.defs.ActivityLabel | { $type: string } | undefined, t: (key: string) => string, ): string | null { if (!activity) return null; if ("name" in activity && activity.name) return activity.name; if ("label" in activity) return t(ACTIVITY_I18N_KEYS[activity.label] ?? activity.label); return null; } export function StreamInfo({ store, user, liveness, chatOpen, onToggleChat, avatar, }: { store: LivestreamStore; user: string; liveness: Liveness; chatOpen: boolean; onToggleChat: () => void; avatar?: string; }) { const { t } = useTranslation("common"); const state = useLivestreamStore( store, useShallow((s) => ({ livestream: s.livestream, viewers: s.viewers, })), ); const { state: sessionState } = useSession(); const followUser = useStore((s) => s.followUser); const [following, setFollowing] = useState(false); const record = state.livestream?.record; const author = state.livestream?.author; const title = record?.title || user; const activity = activityLabel(record?.activity, t); const tags = record?.tags; const isLive = liveness === "live"; const node = useStreamplaceUrl(); const handleFollow = async () => { if (!author?.did) return; setFollowing(true); try { await followUser(author.did); } catch (e) { console.error("Failed to follow:", e); } finally { setFollowing(false); } }; return (
{author?.displayName || author?.handle || user} {author?.handle && author.displayName && ( @{author.handle} )} {isLive && state.viewers != null && ( {t("watching-count", { count: state.viewers })} )}

{title}

{activity && ( {activity} )} {tags?.map((tag) => ( {tag.startsWith("lang:") ? tag.slice(5).toUpperCase() : tag} ))}
{sessionState.status === "authenticated" && sessionState.session.did !== author?.did && ( )}
); } function assembleShareLink( nodeBaseUrl: string, type: "vod" | "live", embed?: boolean, rkey?: string, ): string { const url = new URL(nodeBaseUrl); if (type === "vod" && rkey) { url.pathname = `/${type}/vod/${rkey}`; } else { url.pathname = `/${type}`; } url.pathname += embed ? "/embed" : ""; return url.toString(); } // copy button that has a dropdown for multiple platforms export function CopyButton({ nodeBaseURL, type, rkey, className, variant = "outline", size = "icon-lg", }: { nodeBaseURL?: string; type: "vod" | "live"; rkey?: string; className?: string; variant?: "outline" | "ghost"; size?: "icon-lg" | "icon-touch"; }) { const { t } = useTranslation("common"); const [copied, setCopied] = useState(false); const share = nodeBaseURL ? assembleShareLink(nodeBaseURL, type, false, rkey) : undefined; const embedShare = nodeBaseURL ? assembleShareLink(nodeBaseURL, type, true, rkey) : undefined; const handleCopy = (link: string) => { navigator.clipboard .writeText(link) .then(() => { setCopied(true); window.setTimeout(() => setCopied(false), 1600); }) .catch(() => setCopied(false)); }; return ( <> {copied ? ( ) : ( )} {t("share-copy")} { if (share) { handleCopy(share); } }} > {t("share-copy-link")} { if (embedShare) { handleCopy( ``, ); } }} > {t("share-copy-embed-code")} { if (embedShare) { handleCopy(embedShare); } }} > {t("share-copy-embed-url")} ); }