diff --git a/js/src/features/compose/controls-indicator.tsx b/js/src/features/compose/controls-indicator.tsx index f5a7605..146b792 100644 --- a/js/src/features/compose/controls-indicator.tsx +++ b/js/src/features/compose/controls-indicator.tsx @@ -1,38 +1,58 @@ -import { Video, Mic } from "lucide-react"; -import { cn } from "@/lib/utils"; +import { Video, Mic, Paperclip } from "lucide-react"; import { useMediaSettingsStore } from "@/stores/media-settings-store"; -import { Button } from "@/components/ui/button"; interface ControlsIndicatorProps { type: "reply" | "new"; + attachmentCount?: number; + onOpenAttachments?: () => void; + showEscape?: boolean; } -export default function ControlsIndicator({ type }: ControlsIndicatorProps) { +export default function ControlsIndicator({ + type, + attachmentCount = 0, + showEscape = false, + onOpenAttachments, +}: ControlsIndicatorProps) { const recordingMode = useMediaSettingsStore((s) => s.recordingMode); const setRecordingMode = useMediaSettingsStore((s) => s.setRecordingMode); return ( -
-
- Hold{" "} - - ` +
+ {showEscape && ( + + + Esc + {" "} + back + + )} + + + Hold ` {" "} - to {type === "reply" ? "reply " : "start "} - ยท Press{" "} - + to {type === "reply" ? "reply" : "start"} + + + T {" "} - for text -
- + )} + +
); } diff --git a/js/src/features/network-root.tsx b/js/src/features/network-root.tsx index f7af48b..1b918cf 100644 --- a/js/src/features/network-root.tsx +++ b/js/src/features/network-root.tsx @@ -18,7 +18,7 @@ export default function NetworkRoot() { -
+
diff --git a/js/src/features/particles/attachments-dialog.tsx b/js/src/features/particles/attachments-dialog.tsx new file mode 100644 index 0000000..cf1c5d1 --- /dev/null +++ b/js/src/features/particles/attachments-dialog.tsx @@ -0,0 +1,45 @@ +import type { Particle } from "@/api/types"; +import { + ImageAttachment, + FileAttachment, +} from "@/features/particles/particle-attachments"; +import { + Dialog, + DialogContent, + DialogTitle, +} from "@/components/ui/dialog"; +import { VisuallyHidden } from "radix-ui"; + +type FileParticle = Extract; + +interface AttachmentsDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + attachments: FileParticle[]; +} + +export function AttachmentsDialog({ + open, + onOpenChange, + attachments, +}: AttachmentsDialogProps) { + return ( + + + + Attachments + +
+ {attachments.map((attachment) => { + const isImage = attachment.properties.mime_type.startsWith("image/"); + return isImage ? ( + + ) : ( + + ); + })} +
+
+
+ ); +} diff --git a/js/src/features/particles/media-particle-view.tsx b/js/src/features/particles/media-particle-view.tsx index aada922..6529130 100644 --- a/js/src/features/particles/media-particle-view.tsx +++ b/js/src/features/particles/media-particle-view.tsx @@ -77,8 +77,8 @@ export function MediaParticleView({ }; const attachmentOverlay = attachments.length > 0 && ( -
- +
+
); diff --git a/js/src/features/particles/particle-attachments.tsx b/js/src/features/particles/particle-attachments.tsx index 5d59abc..684c364 100644 --- a/js/src/features/particles/particle-attachments.tsx +++ b/js/src/features/particles/particle-attachments.tsx @@ -1,4 +1,4 @@ -import { Download, ExternalLink, FileIcon } from "lucide-react"; +import { Download, ExternalLink, FileIcon, ImageIcon } from "lucide-react"; import type { Particle } from "@/api/types"; import { useDownloadUrl } from "@/hooks/use-download-url"; import { Skeleton } from "@/components/ui/skeleton"; @@ -9,6 +9,7 @@ type FileParticle = Extract; interface ParticleAttachmentsProps { attachments: FileParticle[]; + variant?: "inline" | "compact"; } function formatFileSize(bytes: number): string { @@ -17,7 +18,7 @@ function formatFileSize(bytes: number): string { return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } -function ImageAttachment({ particle }: { particle: FileParticle }) { +export function ImageAttachment({ particle }: { particle: FileParticle }) { const { data: url, isLoading } = useDownloadUrl(particle.properties.object_id); if (isLoading || !url) { @@ -59,7 +60,7 @@ function ImageAttachment({ particle }: { particle: FileParticle }) { ); } -function FileAttachment({ particle }: { particle: FileParticle }) { +export function FileAttachment({ particle }: { particle: FileParticle }) { const { data: url } = useDownloadUrl(particle.properties.object_id); const handleOpen = (e: React.MouseEvent) => { @@ -117,9 +118,57 @@ function FileAttachment({ particle }: { particle: FileParticle }) { ); } -export function ParticleAttachments({ attachments }: ParticleAttachmentsProps) { +function CompactAttachmentItem({ particle }: { particle: FileParticle }) { + const isImage = particle.properties.mime_type.startsWith("image/"); + const { data: url } = useDownloadUrl(particle.properties.object_id); + + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (url) window.electronLink.openExternal(url); + }; + + return ( + + ); +} + +export function ParticleAttachments({ attachments, variant = "inline" }: ParticleAttachmentsProps) { if (attachments.length === 0) return null; + if (variant === "compact") { + return ( +
+ {attachments.map((attachment) => ( + + ))} +
+ ); + } + return (
diff --git a/js/src/features/particles/stream-view.tsx b/js/src/features/particles/stream-view.tsx index edd3d24..133ad8a 100644 --- a/js/src/features/particles/stream-view.tsx +++ b/js/src/features/particles/stream-view.tsx @@ -204,7 +204,7 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {

No particles in this stream yet

- + - {/* Bottom bar: metadata left, controls right */} -
- {/* Left: seen indicator + exit countdown */} -
- {currentParticle && ( - - )} - {exitRemainingMs !== null && ( - - Closing in {Math.ceil(exitRemainingMs / 1000)}s - - )} -
+ {/* Bottom-left: metadata */} +
+ {currentParticle && ( + + )} + {exitRemainingMs !== null && ( + + Closing in {Math.ceil(exitRemainingMs / 1000)}s + + )} +
- {/* Right: recording controls */} - + {/* Bottom-center: controls */} +
+
); @@ -315,27 +317,8 @@ function TopBar({ networkId, particle, streamParticle }: { networkId: string; pa - - navigate("/")} - > - - - - - - navigate(`/${networkId}`)} - > - - - - {streamParticle && ( <> - {getParticleDisplayName(streamParticle)} diff --git a/js/src/features/particles/transcript-overlay.tsx b/js/src/features/particles/transcript-overlay.tsx index a8d928d..8746b9a 100644 --- a/js/src/features/particles/transcript-overlay.tsx +++ b/js/src/features/particles/transcript-overlay.tsx @@ -75,7 +75,7 @@ export function TranscriptOverlay({ return (

{activeChunk.map((word, i) => {