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 308 lines 9.1 kB view raw
1import { useNavigation } from "@react-navigation/native"; 2import { 3 PlayerUI, 4 Text, 5 Toast, 6 useAvatars, 7 useCameraToggle, 8 useLivestreamInfo, 9 usePlayerDimensions, 10 usePlayerStore, 11 useSegmentDimensions, 12 View, 13 zero, 14} from "@streamplace/components"; 15import { ChevronLeft, SwitchCamera, VolumeX } from "lucide-react-native"; 16import { useEffect, useRef, useState } from "react"; 17import { Image, Pressable, TouchableWithoutFeedback } from "react-native"; 18import Animated, { 19 useAnimatedStyle, 20 useSharedValue, 21 withTiming, 22} from "react-native-reanimated"; 23import { MobileChatPanel } from "./chat"; 24import { useResponsiveLayout } from "./useResponsiveLayout"; 25 26const { borders, colors, gap, h, layout, position, w, bottom, px, py, r } = 27 zero; 28 29export function MobileUi() { 30 const navigation = useNavigation(); 31 const { 32 ingest, 33 profile, 34 title, 35 setTitle, 36 showCountdown, 37 setShowCountdown, 38 recordSubmitted, 39 setRecordSubmitted, 40 ingestStarting, 41 setIngestStarting, 42 toggleGoLive, 43 } = useLivestreamInfo(); 44 const { width, height } = usePlayerDimensions(); 45 const { isPlayerRatioGreater } = useSegmentDimensions(); 46 const { doSetIngestCamera } = useCameraToggle(); 47 const avatars = useAvatars(profile?.did ? [profile?.did] : []); 48 49 const muteWasForced = usePlayerStore((state) => state.muteWasForced); 50 const setMuteWasForced = usePlayerStore((state) => state.setMuteWasForced); 51 const setMuted = usePlayerStore((state) => state.setMuted); 52 53 const { shouldShowFloatingMetrics, safeAreaInsets } = useResponsiveLayout(); 54 const [showLoading, setShowLoading] = useState(false); 55 56 useEffect(() => { 57 return () => { 58 if (ingestStarting) { 59 setIngestStarting(false); 60 } 61 }; 62 }, [ingestStarting, setIngestStarting]); 63 64 useEffect(() => { 65 if (recordSubmitted) setShowLoading(false); 66 }, [recordSubmitted]); 67 68 const isSelfAndNotLive = ingest === "new"; 69 const isLive = ingest !== null && ingest !== "new"; 70 71 const FADE_OUT_DELAY = 4000; 72 const fadeOpacity = useSharedValue(1); 73 const fadeTimeout = useRef<NodeJS.Timeout | null>(null); 74 75 const resetFadeTimer = () => { 76 fadeOpacity.value = withTiming(1, { duration: 200 }); 77 if (fadeTimeout.current) clearTimeout(fadeTimeout.current); 78 fadeTimeout.current = setTimeout(() => { 79 fadeOpacity.value = withTiming(0, { duration: 400 }); 80 }, FADE_OUT_DELAY); 81 }; 82 83 useEffect(() => { 84 resetFadeTimer(); 85 return () => { 86 if (fadeTimeout.current) clearTimeout(fadeTimeout.current); 87 }; 88 }, []); 89 90 const animatedFadeStyle = useAnimatedStyle(() => ({ 91 opacity: shouldShowFloatingMetrics ? 1 : fadeOpacity.value, 92 })); 93 94 return ( 95 <> 96 <TouchableWithoutFeedback onPress={resetFadeTimer}> 97 <Animated.View 98 style={[ 99 layout.position.absolute, 100 h.percent[100], 101 w.percent[100], 102 animatedFadeStyle, 103 ]} 104 > 105 {/* Main UI Overlay */} 106 <View 107 style={[layout.position.absolute, h.percent[100], w.percent[100]]} 108 > 109 {/* Top Left - Back Button and Profile */} 110 <View 111 style={[ 112 { 113 padding: 3, 114 paddingRight: 8, 115 backgroundColor: "rgba(90,90,90, 0.25)", 116 borderRadius: 12, 117 }, 118 r[2], 119 layout.position.absolute, 120 position.left[2], 121 { top: 12 }, 122 ]} 123 > 124 <View style={[layout.flex.row, layout.flex.center, gap.all[2]]}> 125 <Pressable 126 onPress={() => { 127 navigation.canGoBack() 128 ? navigation.goBack() 129 : navigation.navigate("Home", { screen: "StreamList" }); 130 }} 131 > 132 <ChevronLeft color="white" /> 133 </Pressable> 134 {shouldShowFloatingMetrics && ( 135 <> 136 <Image 137 source={ 138 profile?.did 139 ? { url: avatars[profile?.did]?.avatar } 140 : require("assets/images/goose.png") 141 } 142 width={32} 143 height={32} 144 style={[ 145 { 146 width: 36, 147 height: 36, 148 backgroundColor: "green", 149 }, 150 { borderRadius: 999 }, 151 borders.width.thin, 152 borders.color.gray[700], 153 ]} 154 /> 155 <Text>{profile?.handle}</Text> 156 </> 157 )} 158 </View> 159 </View> 160 161 {shouldShowFloatingMetrics && ( 162 <View 163 style={[ 164 { 165 padding: 9, 166 backgroundColor: "rgba(90,90,90, 0.3)", 167 borderRadius: 12, 168 }, 169 r[2], 170 layout.position.absolute, 171 position.right[14], 172 { top: 12 }, 173 gap.all[4], 174 ]} 175 > 176 <PlayerUI.Viewers /> 177 </View> 178 )} 179 180 <View 181 style={[ 182 { 183 padding: 13, 184 backgroundColor: "rgba(0, 0, 0, 0.5)", 185 borderRadius: 12, 186 }, 187 r[2], 188 layout.position.absolute, 189 position.right[1], 190 { top: 8 }, 191 gap.all[4], 192 ]} 193 > 194 {ingest === null ? ( 195 <PlayerUI.ContextMenu /> 196 ) : ( 197 <Pressable onPress={doSetIngestCamera}> 198 <SwitchCamera size={32} color={colors.gray[200]} /> 199 </Pressable> 200 )} 201 </View> 202 203 {shouldShowFloatingMetrics && isLive && ( 204 <View 205 style={[ 206 layout.position.absolute, 207 { top: safeAreaInsets.top + 112 }, 208 position.left[0], 209 position.right[0], 210 layout.flex.column, 211 layout.flex.center, 212 ]} 213 > 214 <PlayerUI.MetricsPanel 215 showMetrics={isLive || isSelfAndNotLive} 216 /> 217 </View> 218 )} 219 </View> 220 221 {isSelfAndNotLive && ( 222 <PlayerUI.InputPanel 223 title={title} 224 setTitle={setTitle} 225 ingestStarting={ingestStarting} 226 toggleGoLive={toggleGoLive} 227 /> 228 )} 229 230 <PlayerUI.CountdownOverlay 231 visible={showCountdown} 232 width={width} 233 height={height - 150} 234 onDone={() => { 235 if (!recordSubmitted && title != "") { 236 setShowLoading(true); 237 } 238 setShowCountdown(false); 239 }} 240 /> 241 <PlayerUI.LoadingOverlay 242 visible={showLoading} 243 width={width} 244 height={height - 150} 245 subtitle="We're setting up your stream." 246 /> 247 248 <Toast 249 open={recordSubmitted} 250 onOpenChange={setRecordSubmitted} 251 title="You're live!" 252 description="We're notifying your followers that you just went live." 253 duration={5} 254 /> 255 </Animated.View> 256 </TouchableWithoutFeedback> 257 258 {!isSelfAndNotLive && ( 259 <MobileChatPanel isPlayerRatioGreater={isPlayerRatioGreater} /> 260 )} 261 {muteWasForced && ( 262 <View 263 style={[ 264 layout.position.absolute, 265 position.top[14], 266 position.right[2], 267 layout.flex.column, 268 layout.flex.center, 269 ]} 270 > 271 <Pressable 272 onPress={() => { 273 if (muteWasForced) { 274 setMuted(false); 275 setMuteWasForced(false); 276 } 277 }} 278 style={[ 279 { 280 flexDirection: "row", 281 alignItems: "center", 282 gap: 8, 283 }, 284 ]} 285 > 286 <Text color="muted" size="sm"> 287 Tap to unmute 288 </Text> 289 <View 290 style={[ 291 { 292 padding: 4, 293 backgroundColor: "rgba(50, 30, 30, 0.4)", 294 borderRadius: 999, 295 borderWidth: 2, 296 borderColor: "rgba(255, 120, 120, 0.2)", 297 }, 298 ]} 299 > 300 <VolumeX size="24" color="rgba(255,120,120,0.8)" /> 301 </View> 302 </Pressable> 303 </View> 304 )} 305 <PlayerUI.AutoplayButton /> 306 </> 307 ); 308}