feat: show seen indicator on particles
This commit is contained in:
@@ -2,11 +2,12 @@ import { Video, Mic } from "lucide-react";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useMediaSettingsStore } from "@/stores/media-settings-store";
|
import { useMediaSettingsStore } from "@/stores/media-settings-store";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { PropsWithChildren } from "react";
|
||||||
|
|
||||||
interface ControlsIndicatorProps {
|
interface ControlsIndicatorProps extends PropsWithChildren {
|
||||||
type: "reply" | "new";
|
type: "reply" | "new";
|
||||||
}
|
}
|
||||||
export default function ControlsIndicator({ type }: ControlsIndicatorProps) {
|
export default function ControlsIndicator({ type, children }: ControlsIndicatorProps) {
|
||||||
const recordingMode = useMediaSettingsStore((s) => s.recordingMode);
|
const recordingMode = useMediaSettingsStore((s) => s.recordingMode);
|
||||||
const setRecordingMode = useMediaSettingsStore((s) => s.setRecordingMode);
|
const setRecordingMode = useMediaSettingsStore((s) => s.setRecordingMode);
|
||||||
|
|
||||||
@@ -39,7 +40,10 @@ export default function ControlsIndicator({ type }: ControlsIndicatorProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</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">
|
<div className="text-muted-foreground">
|
||||||
Hold{" "}
|
Hold{" "}
|
||||||
<kbd
|
<kbd
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import { PlaybackPageIndicator } from "@/features/playback/playback-page-indicat
|
|||||||
import { MediaParticleView } from "@/features/playback/media-particle-view";
|
import { MediaParticleView } from "@/features/playback/media-particle-view";
|
||||||
import { TextParticleView } from "@/features/playback/text-particle-view";
|
import { TextParticleView } from "@/features/playback/text-particle-view";
|
||||||
import { FallbackParticleView } from "@/features/playback/fallback-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 ControlsIndicator from "@/features/compose/controls-indicator";
|
||||||
import { updateStreamPlaybackMarker } from "@/lib/firestore-particles";
|
import { updateStreamPlaybackMarker } from "@/lib/firestore-particles";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { useNetwork } from "@/hooks/use-networks";
|
||||||
|
|
||||||
// --- Playback reducer ---
|
// --- Playback reducer ---
|
||||||
|
|
||||||
@@ -382,8 +384,45 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
|
|||||||
|
|
||||||
{/* Bottom overlay: stream info + reply */}
|
{/* Bottom overlay: stream info + reply */}
|
||||||
<div className="absolute right-0 left-0 bottom-0 z-10">
|
<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>
|
||||||
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,3 +7,8 @@ export function useNetworks() {
|
|||||||
queryFn: () => apiClient.listNetworks(),
|
queryFn: () => apiClient.listNetworks(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useNetwork(networkId: string) {
|
||||||
|
const { data: networks } = useNetworks();
|
||||||
|
return networks?.find((n) => n.id === networkId) || null;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user