feat: support image lightbox viewer and file download (#151)

* feat: add lightbox overlay with nice mechanics

* ui tweak for top bar in stream view

* support locally downloading attachments
This commit was merged in pull request #151.
This commit is contained in:
Arjun Patel
2026-04-12 14:14:23 -07:00
committed by GitHub
parent dbe61e447a
commit 5b6924f7ca
10 changed files with 602 additions and 146 deletions
+10 -25
View File
@@ -24,6 +24,7 @@ import { StreamPresenceProvider, useStreamPresence, useStreamComposing, useStrea
import { ComposingIndicator } from "@/components/composing-indicator";
import { cn } from "@/lib/utils";
import { useMount } from "react-use";
import { usePlaybackStore } from "@/stores/playback-store";
function getReactions(particle: Particle): Record<string, string[]> | undefined {
if (isParticleDeleted(particle)) return undefined;
@@ -192,6 +193,8 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
const [composeActive, setComposeActive] = useState(false);
const [composeStep, setComposeStep] = useState<ComposeStep>("idle");
const playbackSuspended = usePlaybackStore((s) => s.suspendCount > 0);
const playbackBlocked = composeActive || playbackSuspended;
const [progress, setProgress] = useState(0);
const [fastPlayback, setFastPlayback] = useState(false);
const [showKeybindings, setShowKeybindings] = useState(false);
@@ -243,11 +246,11 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
setProgress(0);
}, [currentParticle?.id]);
// Pause/resume playback when compose overlay opens/closes
// Pause/resume playback when compose overlay or lightbox is open.
useEffect(() => {
if (composeActive) pause();
if (playbackBlocked) pause();
else resume();
}, [composeActive, pause, resume]);
}, [playbackBlocked, pause, resume]);
// Playback keyboard: arrows, escape, hold-space-to-pause
useEffect(() => {
@@ -261,7 +264,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
}
const handleKeyDown = (e: KeyboardEvent) => {
if (composeActive) return;
if (playbackBlocked) return;
if (isInputTarget(e)) return;
switch (e.key) {
@@ -313,7 +316,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
};
const handleKeyUp = (e: KeyboardEvent) => {
if (composeActive) return;
if (playbackBlocked) return;
if (isInputTarget(e)) return;
if (e.key === " ") {
@@ -333,22 +336,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
window.removeEventListener("keyup", handleKeyUp);
};
},
[composeActive, next, prev, pause, resume, navigate, networkId, streamParticle.id, setShowKeybindings, handleToggleReaction, recordingMode, setRecordingMode],
);
// Click-to-navigate: left 30% = prev, right 70% = next
// Suppressed when the user has selected text (drag-to-select)
const handlePlaybackClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const selection = window.getSelection();
if (selection && selection.toString().length > 0) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
if (x < 0.3) prev();
else if (x > 0.7) next();
},
[prev, next],
[playbackBlocked, next, prev, pause, resume, navigate, networkId, streamParticle.id, setShowKeybindings, handleToggleReaction, recordingMode, setRecordingMode],
);
if (children.length === 0) {
@@ -430,10 +418,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
{/* Main playback area */}
<div className="flex-1 overflow-hidden">
{currentParticle && (
<div
className="relative flex h-full w-full cursor-pointer items-center justify-center"
onClick={handlePlaybackClick}
>
<div className="relative flex h-full w-full cursor-pointer items-center justify-center">
{renderParticle(currentParticle)}
{fastPlayback && (