Files
llink/js/mobile/src/features/stream-view/StreamTopActions.tsx
T
2026-04-29 17:00:21 -07:00

111 lines
3.5 KiB
TypeScript

import { Pressable, Text, View } from "react-native";
import { EllipsisVertical, Globe, Maximize2, Minimize2 } from "lucide-react-native";
import type { Human, Particle } from "@/api/types";
import { parseVisibleTo } from "@/lib/stream-visibility";
import { cn } from "@/lib/utils";
import { Avatar } from "@/components/Avatar";
import { useStreamPresence } from "./stream-presence-context";
interface StreamTopActionsProps {
networkId: string;
streamParticle: Particle & { type: "stream" };
humans: Human[];
videoFit: "cover" | "contain";
onToggleVideoFit: () => void;
onOpenMembers: () => void;
onOpenActions: () => void;
/** True when current particle is a video — fit toggle hidden otherwise. */
showFitToggle: boolean;
}
const MAX_AVATARS = 3;
/**
* Top-right cluster on StreamView: visibility avatars (with presence ring),
* fit/fill toggle, and actions menu trigger. Mirrors desktop's stream-top-bar
* but compact for the mobile chrome.
*/
export function StreamTopActions({
networkId,
streamParticle,
humans,
videoFit,
onToggleVideoFit,
onOpenMembers,
onOpenActions,
showFitToggle,
}: StreamTopActionsProps) {
const { onlineHumanIds } = useStreamPresence();
const visibility = parseVisibleTo(streamParticle.visible_to, networkId);
const memberIds =
visibility.mode === "network"
? humans.map((h) => h.id)
: visibility.humanIds;
const shown = memberIds.slice(0, MAX_AVATARS);
const overflow = memberIds.length - shown.length;
return (
<View className="flex-row items-center gap-1.5">
<Pressable
onPress={onOpenMembers}
accessibilityLabel="Stream members"
className="bg-white/10 active:bg-white/20 rounded-full px-2 py-1 flex-row items-center gap-1"
>
{visibility.mode === "network" && memberIds.length === 0 ? (
<Globe color="rgba(255,255,255,0.85)" size={14} />
) : (
<View className="flex-row">
{shown.map((id, idx) => (
<View
key={id}
style={{ marginLeft: idx === 0 ? 0 : -8 }}
>
{/* The stack ring matches the chrome's translucent bg so it
reads as a separator without painting hard black halos. */}
<Avatar
humanId={id}
humans={humans}
size="xs"
online={onlineHumanIds.has(id)}
/>
</View>
))}
</View>
)}
{overflow > 0 ? (
<Text className="text-white/70 text-[10px] font-medium ml-0.5">
+{overflow}
</Text>
) : null}
</Pressable>
{showFitToggle ? (
<Pressable
onPress={onToggleVideoFit}
accessibilityLabel={
videoFit === "cover" ? "Fit video to screen" : "Fill screen with video"
}
className={cn(
"h-8 w-8 items-center justify-center rounded-full",
"bg-white/10 active:bg-white/20",
)}
>
{videoFit === "cover" ? (
<Minimize2 color="white" size={15} strokeWidth={1.8} />
) : (
<Maximize2 color="white" size={15} strokeWidth={1.8} />
)}
</Pressable>
) : null}
<Pressable
onPress={onOpenActions}
accessibilityLabel="More actions"
className="h-8 w-8 items-center justify-center rounded-full bg-white/10 active:bg-white/20"
>
<EllipsisVertical color="white" size={16} strokeWidth={1.8} />
</Pressable>
</View>
);
}