Skip to content
Source
Sign inSign up

InlineAudioPreview.tsx

60 lines2.0 KB

1import type { AssetResource, EnvironmentId } from "@t3tools/contracts";
2import { useEffect, useRef, useState } from "react";
3​
4import { useAssetUrlState } from "~/assets/assetUrls";
5​
6import { AudioPreview } from "./AudioPreview";
7​
8export function InlineAudioPreview(props: {
9 readonly name: string;
10 readonly environmentId?: EnvironmentId;
11 readonly resource?: AssetResource;
12 readonly src?: string;
13 readonly copyMarkdown?: string;
14 readonly onOpen?: () => void;
15 readonly variant?: "chip" | "bubble";
16}) {
17 const container = useRef<HTMLSpanElement>(null);
18 const [visible, setVisible] = useState(false);
19 const [failedSrc, setFailedSrc] = useState<string | null>(null);
20 const asset = useAssetUrlState(
21 visible ? (props.environmentId ?? null) : null,
22 visible ? (props.resource ?? null) : null,
23 );
24 const src = visible ? (asset._tag === "Success" ? asset.url : props.src) : undefined;
25 const failed = asset._tag === "Failure" || (src !== undefined && failedSrc === src);
26​
27 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- IntersectionObserver defers loading the audio until the element scrolls into view
28 useEffect(() => {
29 const element = container.current;
30 if (!element) return;
31 const observer = new IntersectionObserver((entries) => {
32 if (entries.some((entry) => entry.isIntersecting)) {
33 setVisible(true);
34 observer.disconnect();
35 }
36 });
37 observer.observe(element);
38 return () => observer.disconnect();
39 }, []);
40​
41 return (
42 <span ref={container} data-markdown-copy={props.copyMarkdown}>
43 <AudioPreview
44 compact
45 variant={props.variant ?? "chip"}
46 name={props.name}
47 src={src ?? ""}
48 revision={
49 props.resource
50 ? JSON.stringify([props.environmentId, props.resource])
51 : (props.src ?? null)
52 }
53 failed={failed}
54 onOpen={props.onOpen}
55 onRetry={() => setFailedSrc(null)}
56 onError={() => setFailedSrc(src ?? null)}
57 />
58 </span>
59 );