mobile v0.1 with deployment for ios (#191)

* stage 1: project init

* stage 2: skeleton with navigation

* step 2.5: streams list

* step 4: stream playback experience

* step 5-6: compose experience

* fix: broken record

* transcode media particles to mp4

* build: reproducible go generate

* build: rename skaffold module for particle processor worker

* infra: increase particle processor worker resources

Was dealing with OOM errors

* tweaks to mobile

* log transcode work

* view on desktop placeholder

* tweak padding

* cap video resolution to save on memory

* infra: bump memory limits as insurance

* ux improvements

* update bundle id for mobile

* config for mobile
This commit was merged in pull request #191.
This commit is contained in:
Arjun Patel
2026-04-29 17:39:11 -07:00
committed by GitHub
parent 3a11a82cd3
commit e3461dd5cd
110 changed files with 14682 additions and 22 deletions
@@ -0,0 +1,110 @@
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>
);
}