Live video on the AT Protocol
1import { formatHandleWithAt, zero } from "@streamplace/components";
2import ThumbnailSelector from "components/thumbnail-selector";
3import { useCaptureVideoFrame } from "hooks/useCaptureVideoFrame";
4import { useLiveUser } from "hooks/useLiveUser";
5import { useEffect, useState } from "react";
6import {
7 Platform,
8 Pressable,
9 ScrollView,
10 Text,
11 TextInput,
12 useWindowDimensions,
13 View,
14} from "react-native";
15import { useStore } from "store";
16import { useNewLivestream, useUserProfile } from "store/hooks";
17
18const isWeb = Platform.OS === "web";
19
20export default function CreateLivestream() {
21 const createLivestreamRecord = useStore(
22 (state) => state.createLivestreamRecord,
23 );
24 const streamplaceUrl = useStore((state) => state.url);
25 // Note: Toast functionality removed, would need simple alert replacement
26 const userIsLive = useLiveUser();
27 const [title, setTitle] = useState("");
28 const [loading, setLoading] = useState(false);
29 const [customThumbnail, setCustomThumbnail] = useState<Blob | undefined>(
30 undefined,
31 );
32 const profile = useUserProfile();
33 const newLivestream = useNewLivestream();
34 const captureFrame = useCaptureVideoFrame();
35 const { width } = useWindowDimensions();
36
37 // Responsive layout logic
38 const isWide = width > 1020;
39 const useTwoColumns = isWide;
40
41 useEffect(() => {
42 if (newLivestream?.record) {
43 // Would show toast: "Livestream announced" with newLivestream.record.title
44 setTitle("");
45 setCustomThumbnail(undefined);
46 }
47 }, [newLivestream?.record]);
48
49 useEffect(() => {
50 if (newLivestream?.error) {
51 // Would show toast: "Error creating livestream" with error message
52 }
53 }, [newLivestream?.error]);
54
55 const disabled = !userIsLive || loading || title === "";
56
57 const handleSubmit = async () => {
58 setLoading(true);
59 try {
60 let thumbnailToUse = customThumbnail;
61 if (!thumbnailToUse && isWeb && captureFrame) {
62 const capturedFrame = await captureFrame(1280, 0.85);
63 if (capturedFrame) {
64 thumbnailToUse = capturedFrame;
65 }
66 }
67
68 await createLivestreamRecord(title, thumbnailToUse);
69 } catch (error) {
70 console.error("Error creating livestream:", error);
71 // Would show toast: "Error creating livestream"
72 } finally {
73 setLoading(false);
74 }
75 };
76
77 const buttonText = loading
78 ? "Loading..."
79 : !userIsLive
80 ? "Waiting for stream to start..."
81 : "Announce Livestream!";
82
83 return (
84 <ScrollView
85 style={{ width: "60%" }}
86 contentContainerStyle={{
87 flexGrow: 1,
88 justifyContent: "flex-start",
89 paddingVertical: 40,
90 }}
91 showsVerticalScrollIndicator={false}
92 >
93 <View
94 style={[
95 { flexDirection: useTwoColumns ? "row" : "column" },
96 { gap: useTwoColumns ? 48 : 16 },
97 { width: "100%" },
98 { maxWidth: useTwoColumns ? 900 : undefined },
99 { alignSelf: "center" },
100 zero.p[4],
101 { alignItems: useTwoColumns ? "flex-start" : "stretch" },
102 { justifyContent: "center" },
103 ]}
104 >
105 {/* Left column: labels and fields */}
106 <View
107 style={[
108 { flex: 2, minWidth: 0 },
109 { gap: 12 },
110 { width: useTwoColumns ? 500 : "100%" },
111 ]}
112 >
113 <View
114 style={[
115 { flexDirection: "row" },
116 { alignItems: "center" },
117 { width: "100%" },
118 ]}
119 >
120 <Text
121 style={[{ paddingBottom: 8, minWidth: 100, textAlign: "left" }]}
122 >
123 Streamer
124 </Text>
125 <Text style={[{ paddingBottom: 8, fontWeight: "bold" }]}>
126 {profile && formatHandleWithAt(profile)}
127 </Text>
128 </View>
129
130 <View
131 style={[
132 { flexDirection: "row" },
133 { alignItems: "center" },
134 { width: "100%" },
135 ]}
136 >
137 <Text
138 style={[{ paddingBottom: 8, minWidth: 100, textAlign: "left" }]}
139 >
140 Title
141 </Text>
142 <View style={zero.flex.values[1]}>
143 <TextInput
144 value={title}
145 onChangeText={setTitle}
146 maxLength={140}
147 style={[
148 {
149 minHeight: 100,
150 width: "100%",
151 borderWidth: 1,
152 borderColor: "#ccc",
153 borderRadius: 8,
154 padding: 12,
155 textAlignVertical: "top",
156 },
157 ]}
158 multiline
159 />
160 </View>
161 </View>
162
163 <View
164 style={[{ width: "100%" }, { alignItems: "center" }, zero.mt[4]]}
165 >
166 <Pressable
167 disabled={disabled}
168 style={[
169 {
170 opacity: disabled ? 0.5 : 1,
171 width: "100%",
172 backgroundColor: "#0066cc",
173 padding: 16,
174 borderRadius: 8,
175 alignItems: "center",
176 },
177 ]}
178 onPress={handleSubmit}
179 >
180 <Text
181 style={{ color: "white", fontSize: 16, fontWeight: "bold" }}
182 >
183 {buttonText}
184 </Text>
185 </Pressable>
186 </View>
187 </View>
188
189 {/* Right column: thumbnail */}
190 <View
191 style={[
192 { flex: 1, minWidth: 0 },
193 { gap: 16 },
194 { alignItems: "center" },
195 { justifyContent: "flex-start" },
196 { width: useTwoColumns ? 400 : "100%" },
197 {
198 marginTop: 12,
199 ...(useTwoColumns ? {} : { marginLeft: 40 }),
200 },
201 ]}
202 >
203 <View
204 style={[
205 { flexDirection: "column" },
206 { alignItems: "center" },
207 { width: "100%" },
208 ]}
209 >
210 <Text
211 style={[
212 {
213 paddingBottom: 0,
214 lineHeight: 18,
215 fontWeight: "bold",
216 marginBottom: 8,
217 },
218 ]}
219 >
220 Custom Thumbnail (Optional)
221 </Text>
222 <View style={[{ maxWidth: 400, width: "100%" }]}>
223 <ThumbnailSelector onThumbnailSelected={setCustomThumbnail} />
224 </View>
225 </View>
226 </View>
227 </View>
228 </ScrollView>
229 );
230}