Live video on the AT Protocol
1import {
2 Button,
3 ContentRights,
4 ContentWarnings,
5 formatHandle,
6 formatHandleWithAt,
7 layout,
8 PlayerUI,
9 ShareSheet,
10 Text,
11 useAvatars,
12 useDID,
13 useLivestreamInfo,
14 useLivestreamStore,
15 zero,
16} from "@streamplace/components";
17import FollowButton from "components/follow-button";
18import { ChevronLeft, ChevronRight } from "lucide-react-native";
19import { Image, Linking, Pressable, View } from "react-native";
20const { gap, px, py, colors } = zero;
21
22export function BottomMetadata({
23 setShowChat,
24 showChat,
25}: {
26 setShowChat: (show: boolean) => void;
27 showChat: boolean;
28}) {
29 const { profile } = useLivestreamInfo();
30 const avatars = useAvatars(profile?.did ? [profile?.did] : []);
31 const ls = useLivestreamStore((x) => x.livestream);
32 const segment = useLivestreamStore((x) => x.segment);
33
34 const did = useDID();
35
36 // Get content warnings and rights directly from the latest segment
37 const contentWarnings =
38 (segment?.contentWarnings?.warnings as string[]) || [];
39 const contentRights = segment?.contentRights;
40
41 return (
42 <View
43 style={[
44 layout.position.relative,
45 {
46 backgroundColor: "rgba(0, 0, 0, 0.9)",
47 borderTopWidth: 1,
48 borderTopColor: "rgba(255, 255, 255, 0.1)",
49 },
50 px[5],
51 py[3],
52 ]}
53 >
54 <View
55 style={[
56 layout.flex.row,
57 layout.flex.spaceBetween,
58 { height: "100%", flex: "auto" as any },
59 ]}
60 >
61 {/* Left side - Profile info */}
62 <View
63 style={[
64 layout.flex.row,
65 layout.flex.center,
66 gap.all[3],
67 { flex: 1, minWidth: 0 },
68 ]}
69 >
70 {profile?.did && avatars[profile?.did]?.avatar && (
71 <Image
72 key="avatar"
73 source={{
74 uri: avatars[profile?.did]?.avatar,
75 }}
76 style={{ width: 42, height: 42, borderRadius: 999 }}
77 resizeMode="cover"
78 />
79 )}
80 {!(profile?.did && avatars[profile?.did]?.avatar) && (
81 <Image
82 key="avatar"
83 source={require("./../../assets/images/goose.png")}
84 style={{ width: 42, height: 42, borderRadius: 999 }}
85 resizeMode="cover"
86 />
87 )}
88 <View style={{ flex: 1, minWidth: 0 }}>
89 <View
90 style={[layout.flex.row, layout.flex.alignCenter, gap.all[2]]}
91 >
92 <Pressable
93 onPress={() => {
94 if (profile?.handle) {
95 const url = `https://bsky.app/profile/${formatHandle(profile)}`;
96 Linking.openURL(url);
97 }
98 }}
99 >
100 <Text style={{ color: "white", fontWeight: "600" }}>
101 {profile ? formatHandleWithAt(profile) : "@user"}
102 </Text>
103 </Pressable>
104 {did && profile && (
105 <FollowButton streamerDID={profile?.did} currentUserDID={did} />
106 )}
107 </View>
108 <Text
109 style={{ color: colors.gray[400] }}
110 numberOfLines={3}
111 ellipsizeMode="tail"
112 >
113 {ls?.record.title || "Stream Title"}
114 </Text>
115 </View>
116 </View>
117
118 {/* Right side - Viewer count and collapse chat */}
119 <View style={[layout.flex.row, layout.flex.align.center, gap.all[4]]}>
120 <PlayerUI.Viewers />
121 <ShareSheet />
122 <View>
123 <Button
124 variant="outline"
125 size="sm"
126 width="min"
127 style={{ aspectRatio: 1 }}
128 onPress={() => {
129 setShowChat(!showChat);
130 }}
131 >
132 {showChat ? (
133 <ChevronRight color="white" size={16} />
134 ) : (
135 <ChevronLeft color="white" size={16} />
136 )}
137 </Button>
138 </View>
139 </View>
140 </View>
141
142 {/* Content Metadata - Below the main profile/controls bar */}
143 {(contentWarnings.length > 0 ||
144 (contentRights && Object.keys(contentRights).length > 0)) && (
145 <View style={[py[2]]}>
146 <ContentWarnings warnings={contentWarnings} compact={true} />
147 {contentRights && (
148 <ContentRights contentRights={contentRights} compact={true} />
149 )}
150 </View>
151 )}
152 </View>
153 );
154}