Files
llink/js/mobile/src/components/ComposingIndicator.tsx
T

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} />
);
}