Files
llink/js/mobile/src/features/streams/StreamListScreen.tsx
T
2026-04-29 11:24:29 -07:00

158 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
ActivityIndicator,
FlatList,
Pressable,
RefreshControl,
Text,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { toast } from "sonner-native";
import { toUserMessage } from "@/lib/errors";
import { particlePath } from "@/lib/particle-path";
import { useNetwork, useNetworks } from "@/hooks/use-networks";
import { useStreamParticles } from "@/hooks/use-stream-particles";
import type { RootStackScreenProps } from "@/navigation/types";
import { StreamCard } from "./StreamCard";
export function StreamListScreen({
route,
navigation,
}: RootStackScreenProps<"StreamList">) {
const { networkId } = route.params;
const network = useNetwork(networkId);
// Re-fetching the network list is the closest stand-in for a hard refresh —
// Firestore subscriptions are already realtime, so pull-to-refresh mainly
// reassures the user and re-tries network metadata.
const { refetch: refetchNetworks, isRefetching: isRefetchingNetworks } =
useNetworks();
const path = particlePath(networkId, []);
// v1: only show open streams. Closed streams are reachable on desktop —
// an "Archived" surface is a follow-up (PRD §20).
const { streams, isLoading, error } = useStreamParticles(path, {
status: "open",
});
return (
<SafeAreaView className="flex-1 bg-background" edges={["top"]}>
<Header
title={network?.name ?? "Streams"}
onBack={() => navigation.goBack()}
/>
{error ? (
<ErrorState message={toUserMessage(error)} />
) : isLoading && streams.length === 0 ? (
<LoadingState />
) : streams.length === 0 ? (
<EmptyState />
) : (
<FlatList
data={streams}
keyExtractor={(s) => s.id}
contentContainerClassName="p-3 gap-2 pb-24"
refreshControl={
<RefreshControl
refreshing={isRefetchingNetworks}
onRefresh={() => refetchNetworks()}
/>
}
renderItem={({ item }) => (
<StreamCard
particle={item}
networkId={networkId}
onPress={() =>
navigation.navigate("StreamView", {
networkId,
streamId: item.id,
})
}
/>
)}
/>
)}
<ComposeFab
onPress={() => {
// Real compose flow lands in step 5.
toast("New stream — coming soon");
}}
/>
</SafeAreaView>
);
}
function Header({
title,
onBack,
}: {
title: string;
onBack: () => void;
}) {
return (
<View className="flex-row items-center px-3 py-3 border-b border-border">
<Pressable
onPress={onBack}
className="px-2 py-1"
accessibilityLabel="Back"
>
<Text className="text-foreground text-2xl"></Text>
</Pressable>
<Text
className="flex-1 text-center text-foreground text-base font-semibold"
numberOfLines={1}
>
{title}
</Text>
<View className="w-8" />
</View>
);
}
function LoadingState() {
return (
<View className="flex-1 items-center justify-center">
<ActivityIndicator />
</View>
);
}
function EmptyState() {
return (
<View className="flex-1 items-center justify-center px-6">
<Text className="text-foreground text-lg font-medium text-center">
No streams yet.
</Text>
<Text className="text-muted-foreground mt-2 text-center">
Tap the button below to start one voice, video, or text.
</Text>
</View>
);
}
function ErrorState({ message }: { message: string }) {
return (
<View className="flex-1 items-center justify-center px-6">
<Text className="text-destructive text-center">{message}</Text>
<Text className="text-muted-foreground mt-2 text-center text-xs">
Streams reconnect automatically once the network is back.
</Text>
</View>
);
}
function ComposeFab({ onPress }: { onPress: () => void }) {
return (
<View className="absolute bottom-6 right-6">
<Pressable
onPress={onPress}
className="bg-primary h-14 w-14 items-center justify-center rounded-full active:opacity-80"
accessibilityLabel="New stream"
>
<Text className="text-primary-foreground text-3xl leading-none">+</Text>
</Pressable>
</View>
);
}