Live video on the AT Protocol
79
fork

Configure Feed

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

at eli/revert-dev-env 173 lines 5.4 kB view raw
1import { useToastController } from "@tamagui/toast"; 2import ThumbnailSelector from "components/thumbnail-selector"; 3import { 4 createLivestreamRecord, 5 selectNewLivestream, 6 selectUserProfile, 7} from "features/bluesky/blueskySlice"; 8import { useCaptureVideoFrame } from "hooks/useCaptureVideoFrame"; 9import { useLiveUser } from "hooks/useLiveUser"; 10import { useEffect, useState } from "react"; 11import { ScrollView, useWindowDimensions } from "react-native"; 12import { useAppDispatch, useAppSelector } from "store/hooks"; 13import { Button, Label, Paragraph, TextArea, View, isWeb } from "tamagui"; 14 15export default function CreateLivestream() { 16 const dispatch = useAppDispatch(); 17 const toast = useToastController(); 18 const userIsLive = useLiveUser(); 19 const [title, setTitle] = useState(""); 20 const [loading, setLoading] = useState(false); 21 const [customThumbnail, setCustomThumbnail] = useState<Blob | undefined>( 22 undefined, 23 ); 24 const profile = useAppSelector(selectUserProfile); 25 const newLivestream = useAppSelector(selectNewLivestream); 26 const captureFrame = useCaptureVideoFrame(); 27 const { width } = useWindowDimensions(); 28 29 // Responsive layout logic 30 const isWide = width > 1020; 31 const useTwoColumns = isWide; 32 33 useEffect(() => { 34 if (newLivestream?.record) { 35 toast.show("Livestream announced", { 36 message: newLivestream.record.title, 37 }); 38 setTitle(""); 39 setCustomThumbnail(undefined); 40 } 41 }, [newLivestream?.record]); 42 useEffect(() => { 43 if (newLivestream?.error) { 44 toast.show("Error creating livestream", { 45 message: newLivestream.error, 46 }); 47 } 48 }, [newLivestream?.error]); 49 const disabled = !userIsLive || loading || title === ""; 50 51 const handleSubmit = async () => { 52 setLoading(true); 53 try { 54 let thumbnailToUse = customThumbnail; 55 if (!thumbnailToUse && isWeb && captureFrame) { 56 const capturedFrame = await captureFrame(1280, 0.85); 57 if (capturedFrame) { 58 thumbnailToUse = capturedFrame; 59 } 60 } 61 62 await dispatch( 63 createLivestreamRecord({ 64 title, 65 customThumbnail: thumbnailToUse, 66 }), 67 ); 68 } catch (error) { 69 console.error("Error creating livestream:", error); 70 toast.show("Error creating livestream", { 71 message: String(error), 72 }); 73 } finally { 74 setLoading(false); 75 } 76 }; 77 78 const buttonText = loading 79 ? "Loading..." 80 : !userIsLive 81 ? "Waiting for stream to start..." 82 : "Announce Livestream!"; 83 84 return ( 85 <ScrollView 86 style={{ width: "60%" }} 87 contentContainerStyle={{ 88 flexGrow: 1, 89 justifyContent: "flex-start", 90 paddingVertical: 40, 91 }} 92 showsVerticalScrollIndicator={false} 93 > 94 <View 95 flexDirection={useTwoColumns ? "row" : "column"} 96 gap={useTwoColumns ? 48 : 16} 97 w="100%" 98 maxWidth={useTwoColumns ? 900 : undefined} 99 alignSelf="center" 100 p="$4" 101 alignItems={useTwoColumns ? "flex-start" : "stretch"} 102 justifyContent="center" 103 > 104 {/* Left column: labels and fields */} 105 <View f={2} minWidth={0} gap="$3" w={useTwoColumns ? 500 : "100%"}> 106 <Label asChild={true} display="flex"> 107 <View flexDirection="row" alignItems="center" w="100%"> 108 <Paragraph pb="$2" minWidth={100} textAlign="left"> 109 Streamer 110 </Paragraph> 111 <Paragraph pb="$2" fontWeight="bold"> 112 @{profile?.handle} 113 </Paragraph> 114 </View> 115 </Label> 116 <Label asChild={true}> 117 <View flexDirection="row" alignItems="center" w="100%"> 118 <Paragraph pb="$2" minWidth={100} textAlign="left"> 119 Title 120 </Paragraph> 121 <View flex={1}> 122 <TextArea 123 id="livestream-title" 124 value={title} 125 onChangeText={setTitle} 126 size="$4" 127 minHeight={100} 128 maxLength={140} 129 w="100%" 130 /> 131 </View> 132 </View> 133 </Label> 134 <View w="100%" alignItems="center" mt="$4"> 135 <Button 136 disabled={disabled} 137 opacity={disabled ? 0.5 : 1} 138 size="$4" 139 w="100%" 140 onPress={handleSubmit} 141 > 142 {buttonText} 143 </Button> 144 </View> 145 </View> 146 {/* Right column: thumbnail */} 147 <View 148 f={1} 149 minWidth={0} 150 gap="$4" 151 alignItems="center" 152 justifyContent="flex-start" 153 w={useTwoColumns ? 400 : "100%"} 154 style={{ 155 marginTop: 12, 156 ...(useTwoColumns ? {} : { marginLeft: 40 }), 157 }} 158 > 159 <Label asChild={true}> 160 <View flexDirection="column" alignItems="center" w="100%"> 161 <Paragraph pb={0} lineHeight={18} fontWeight="bold" mb="$2"> 162 Custom Thumbnail (Optional) 163 </Paragraph> 164 <View maxWidth={400} w="100%"> 165 <ThumbnailSelector onThumbnailSelected={setCustomThumbnail} /> 166 </View> 167 </View> 168 </Label> 169 </View> 170 </View> 171 </ScrollView> 172 ); 173}