Live video on the AT Protocol
79
fork

Configure Feed

Select the types of activity you want to include in your feed.

at next 230 lines 6.6 kB view raw
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}