From f46c2282433e062bda3da7074399f1159c11090e Mon Sep 17 00:00:00 2001 From: talksik Date: Thu, 19 Mar 2026 15:55:15 -0700 Subject: [PATCH] feat: show seen indicator on particles --- .../features/compose/controls-indicator.tsx | 10 +++-- js/src/features/particles/stream-view.tsx | 43 ++++++++++++++++++- js/src/hooks/use-networks.ts | 5 +++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/js/src/features/compose/controls-indicator.tsx b/js/src/features/compose/controls-indicator.tsx index ffc0101..e46da0a 100644 --- a/js/src/features/compose/controls-indicator.tsx +++ b/js/src/features/compose/controls-indicator.tsx @@ -2,11 +2,12 @@ import { Video, Mic } from "lucide-react"; import { cn } from "@/lib/utils"; import { useMediaSettingsStore } from "@/stores/media-settings-store"; import { Button } from "@/components/ui/button"; +import { PropsWithChildren } from "react"; -interface ControlsIndicatorProps { +interface ControlsIndicatorProps extends PropsWithChildren { type: "reply" | "new"; } -export default function ControlsIndicator({ type }: ControlsIndicatorProps) { +export default function ControlsIndicator({ type, children }: ControlsIndicatorProps) { const recordingMode = useMediaSettingsStore((s) => s.recordingMode); const setRecordingMode = useMediaSettingsStore((s) => s.setRecordingMode); @@ -39,7 +40,10 @@ export default function ControlsIndicator({ type }: ControlsIndicatorProps) { )} -
+ {children ?
{children}
: +
+ } + {children && ยท}
Hold{" "} - + + +
); } + +// Shows a list of avatars of users who have seen the current particle, based on playback markers in the stream particle. +const SeenIndicator = ({ stream, currentParticle, networkId }: { stream: Particle & { type: "stream" }, currentParticle: Particle, networkId: string }) => { + const network = useNetwork(networkId); + const playbackMarkers = stream.playback_markers ?? {}; + + const seenUserIds = Object.entries(playbackMarkers) + .filter(([_, timestamp]) => timestamp.getTime() >= currentParticle.created_at.getTime()) + .map(([userId, _]) => userId); + + const seenUserEmails = seenUserIds + .map((userId) => network?.humans?.find((h) => h.id === userId)?.email) + .filter((email): email is string => !!email); + + if (seenUserIds.length === 0) return null; + + return ( + + {seenUserEmails.map((email) => ( + + + + + {email.split("@")[0].slice(0, 2)} + + + + +

Seen by {email.split("@")[0]}

+
+
+ ))} +
+ ); +} diff --git a/js/src/hooks/use-networks.ts b/js/src/hooks/use-networks.ts index a770741..7e2174d 100644 --- a/js/src/hooks/use-networks.ts +++ b/js/src/hooks/use-networks.ts @@ -7,3 +7,8 @@ export function useNetworks() { queryFn: () => apiClient.listNetworks(), }); } + +export function useNetwork(networkId: string) { + const { data: networks } = useNetworks(); + return networks?.find((n) => n.id === networkId) || null; +}