step 2.5: streams list
This commit is contained in:
@@ -1,34 +1,157 @@
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
Pressable,
|
||||
RefreshControl,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { useNetwork } from "@/hooks/use-networks";
|
||||
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 network = useNetwork(route.params.networkId);
|
||||
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"]}>
|
||||
<View className="flex-row items-center px-3 py-3 border-b border-border">
|
||||
<Pressable onPress={() => navigation.goBack()} className="px-2 py-1">
|
||||
<Text className="text-foreground text-2xl">‹</Text>
|
||||
</Pressable>
|
||||
<Text
|
||||
className="flex-1 text-center text-foreground text-base font-semibold"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{network?.name ?? "Streams"}
|
||||
</Text>
|
||||
<View className="w-8" />
|
||||
</View>
|
||||
<Header
|
||||
title={network?.name ?? "Streams"}
|
||||
onBack={() => navigation.goBack()}
|
||||
/>
|
||||
|
||||
<View className="flex-1 items-center justify-center px-6">
|
||||
<Text className="text-muted-foreground text-center">
|
||||
Stream list — coming in step 3.
|
||||
</Text>
|
||||
</View>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user