feat: improve chat media and notification interactions
fix: reveal spoiler text on tap with animated transitions fix: render and play circle videos correctly on web feat: add quick reply and mark-as-read notification actions fix: stabilize circle recording gestures and chat auxiliary error handling
This commit is contained in:
@@ -1116,36 +1116,49 @@ function renderMessageContent(
|
||||
if (mediaItems.length === 1) {
|
||||
const item = mediaItems[0];
|
||||
const blockViewerOpen = isStickerOrGifMedia(item.url);
|
||||
const isInteractiveCircle = isCircleVideo && item.type === "video";
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<button
|
||||
className={`relative block overflow-hidden bg-slate-950/30 ${isCircleVideo ? "aspect-square w-56 rounded-full" : "rounded-xl"}`}
|
||||
onClick={() => {
|
||||
if (blockViewerOpen || isCircleVideo) {
|
||||
return;
|
||||
}
|
||||
opts.onOpenMedia(item.url, item.type);
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
event.stopPropagation();
|
||||
opts.onAttachmentContextMenu(event, item.url);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{item.type === "image" ? (
|
||||
<img
|
||||
alt="attachment"
|
||||
className={isCircleVideo ? "h-full w-full object-cover" : "max-h-80 rounded-xl object-cover"}
|
||||
draggable={false}
|
||||
src={item.url}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<video className={isCircleVideo ? "h-full w-full object-cover" : "max-h-80 rounded-xl"} muted src={item.url} />
|
||||
<span className="absolute inset-0 flex items-center justify-center text-3xl text-white/85">▶</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{isInteractiveCircle ? (
|
||||
<div
|
||||
className="relative block aspect-square w-56 overflow-hidden rounded-full bg-slate-950/30"
|
||||
onContextMenu={(event) => {
|
||||
event.stopPropagation();
|
||||
opts.onAttachmentContextMenu(event, item.url);
|
||||
}}
|
||||
>
|
||||
<CircleVideoInlinePlayer src={item.url} />
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className={`relative block overflow-hidden bg-slate-950/30 ${isCircleVideo ? "aspect-square w-56 rounded-full" : "rounded-xl"}`}
|
||||
onClick={() => {
|
||||
if (blockViewerOpen || isCircleVideo) {
|
||||
return;
|
||||
}
|
||||
opts.onOpenMedia(item.url, item.type);
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
event.stopPropagation();
|
||||
opts.onAttachmentContextMenu(event, item.url);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{item.type === "image" ? (
|
||||
<img
|
||||
alt="attachment"
|
||||
className={isCircleVideo ? "h-full w-full object-cover" : "max-h-80 rounded-xl object-cover"}
|
||||
draggable={false}
|
||||
src={item.url}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<video className={isCircleVideo ? "h-full w-full object-cover" : "max-h-80 rounded-xl"} muted src={item.url} />
|
||||
<span className="absolute inset-0 flex items-center justify-center text-3xl text-white/85">▶</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{captionText ? (
|
||||
<p className="whitespace-pre-wrap break-words text-sm" dangerouslySetInnerHTML={{ __html: formatMessageHtml(captionText) }} />
|
||||
) : null}
|
||||
@@ -1500,6 +1513,71 @@ async function downloadFileFromUrl(url: string): Promise<void> {
|
||||
window.URL.revokeObjectURL(blobUrl);
|
||||
}
|
||||
|
||||
function CircleVideoInlinePlayer({ src }: { src: string }) {
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
const handlePlay = () => setIsPlaying(true);
|
||||
const handlePause = () => setIsPlaying(false);
|
||||
const handleEnded = () => {
|
||||
setIsPlaying(false);
|
||||
video.currentTime = 0;
|
||||
};
|
||||
video.addEventListener("play", handlePlay);
|
||||
video.addEventListener("pause", handlePause);
|
||||
video.addEventListener("ended", handleEnded);
|
||||
return () => {
|
||||
video.removeEventListener("play", handlePlay);
|
||||
video.removeEventListener("pause", handlePause);
|
||||
video.removeEventListener("ended", handleEnded);
|
||||
};
|
||||
}, [src]);
|
||||
|
||||
async function togglePlayback() {
|
||||
const video = videoRef.current;
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
if (video.paused || video.ended) {
|
||||
try {
|
||||
await video.play();
|
||||
} catch {
|
||||
setIsPlaying(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
video.pause();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="h-full w-full object-cover"
|
||||
playsInline
|
||||
preload="metadata"
|
||||
src={src}
|
||||
/>
|
||||
<span className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-full bg-black/45 text-2xl text-white">
|
||||
{isPlaying ? "❚❚" : "▶"}
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
aria-label={isPlaying ? "Pause video message" : "Play video message"}
|
||||
className="absolute inset-0"
|
||||
onClick={() => void togglePlayback()}
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AudioInlinePlayer({ src, title }: { src: string; title: string }) {
|
||||
const track = useAudioPlayerStore((s) => s.track);
|
||||
const isPlayingGlobal = useAudioPlayerStore((s) => s.isPlaying);
|
||||
|
||||
Reference in New Issue
Block a user