feat: show seen indicator on particles

This commit is contained in:
talksik
2026-03-19 15:55:15 -07:00
parent 40548a1169
commit f46c228243
3 changed files with 53 additions and 5 deletions
@@ -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) {
</>
)}
</Button>
<div className="flex-1" />
{children ? <div className="flex-1">{children}</div> :
<div className="flex-1" />
}
{children && <span className="text-muted-foreground">·</span>}
<div className="text-muted-foreground">
Hold{" "}
<kbd
+41 -2
View File
@@ -9,9 +9,11 @@ import { PlaybackPageIndicator } from "@/features/playback/playback-page-indicat
import { MediaParticleView } from "@/features/playback/media-particle-view";
import { TextParticleView } from "@/features/playback/text-particle-view";
import { FallbackParticleView } from "@/features/playback/fallback-particle-view";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount } from "@/components/ui/avatar";
import ControlsIndicator from "@/features/compose/controls-indicator";
import { updateStreamPlaybackMarker } from "@/lib/firestore-particles";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useNetwork } from "@/hooks/use-networks";
// --- Playback reducer ---
@@ -382,8 +384,45 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
{/* Bottom overlay: stream info + reply */}
<div className="absolute right-0 left-0 bottom-0 z-10">
<ControlsIndicator type={"reply"} />
<ControlsIndicator type={"reply"}>
<SeenIndicator stream={streamParticle} currentParticle={currentParticle} networkId={networkId} />
</ControlsIndicator>
</div>
</div>
);
}
// 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 (
<AvatarGroup>
{seenUserEmails.map((email) => (
<Tooltip key={email}>
<TooltipTrigger asChild>
<Avatar size="sm">
<AvatarFallback>
{email.split("@")[0].slice(0, 2)}
</AvatarFallback>
</Avatar>
</TooltipTrigger>
<TooltipContent>
<p>Seen by {email.split("@")[0]}</p>
</TooltipContent>
</Tooltip>
))}
</AvatarGroup>
);
}
+5
View File
@@ -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;
}