96 lines
2.4 KiB
TypeScript
96 lines
2.4 KiB
TypeScript
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} />
|
|
);
|
|
}
|