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,95 @@
import { useEffect } from "react";
import { Text, View } from "react-native";
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming,
} from "react-native-reanimated";
import type { Human } from "@/api/types";
import { resolveHumanDisplay } from "@/lib/humans";
import type { ComposingUser } from "@/features/stream-view/stream-presence-context";
interface ComposingIndicatorProps {
users: ComposingUser[];
networkHumans?: Human[];
}
/**
* Slim horizontal pill stack pinned just under the metadata header. Each
* pill is the typing/recording indicator for a single user. Rendered above
* the particle canvas with a translucent background so it reads on any
* media. Mobile equivalent of desktop's vertical writing-mode indicator.
*/
export function ComposingIndicator({
users,
networkHumans,
}: ComposingIndicatorProps) {
if (users.length === 0) return null;
return (
<View pointerEvents="none" className="flex-row flex-wrap items-center gap-1.5">
{users.map((u) => {
const { displayName } = resolveHumanDisplay(u.humanId, networkHumans);
const label =
u.mode === "recording"
? `${displayName} is recording`
: u.mode === "screen"
? `${displayName} is sharing`
: `${displayName} is typing`;
return (
<View
key={u.humanId}
className="bg-white/15 flex-row items-center gap-1.5 rounded-full px-2 py-1"
>
<BouncingDots />
<Text className="text-white/80 text-[11px] font-medium">
{label}
</Text>
</View>
);
})}
</View>
);
}
function BouncingDots() {
return (
<View className="flex-row items-end gap-0.5" style={{ height: 8 }}>
<Dot delay={0} />
<Dot delay={150} />
<Dot delay={300} />
</View>
);
}
function Dot({ delay }: { delay: number }) {
const y = useSharedValue(0);
useEffect(() => {
const start = setTimeout(() => {
y.value = withRepeat(
withTiming(-3, {
duration: 360,
easing: Easing.inOut(Easing.quad),
}),
-1,
true,
);
}, delay);
return () => clearTimeout(start);
}, [delay, y]);
const style = useAnimatedStyle(() => ({
transform: [{ translateY: y.value }],
}));
return (
<Animated.View
className="bg-white/85 h-1 w-1 rounded-full"
style={style}
/>
);
}