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:
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user