Live video on the AT Protocol

Merge pull request #735 from streamplace/natb/instant-rotation

fix: make rotation layout adjustment slightly quicker?

authored by

natalie and committed by
GitHub
5fd7b14e 07320c22

+141 -158
+48 -30
js/app/components/mobile/chat.tsx
··· 22 22 import { usePDSAgent } from "@streamplace/components/src/streamplace-store/xrpc"; 23 23 import emojiData from "assets/emoji-data.json"; 24 24 import { ArrowRight } from "lucide-react-native"; 25 + import { useSafeAreaInsets } from "react-native-safe-area-context"; 25 26 const { borderRadius, gap, layout, flex, px, position, bottom } = zero; 26 27 27 - export function DesktopChatPanel({ 28 - chatVisible, 29 - chatPanelWidth, 30 - safeAreaInsets, 31 - }) { 32 - const sidebarOffset = useSharedValue(chatVisible ? 0 : chatPanelWidth); 28 + export function DesktopChatPanel({ chatVisible, chatPanelWidth }) { 29 + let insets = useSafeAreaInsets(); 30 + let panelWidthWithInsets = chatPanelWidth; 31 + const sidebarOffset = useSharedValue(chatVisible ? 0 : panelWidthWithInsets); 32 + const sidebarOpacity = useSharedValue(chatVisible ? 1 : 0); 33 33 34 34 const kb = useKeyboard(); 35 35 36 36 useEffect(() => { 37 37 console.log( 38 38 "Setting sidebar offset x to", 39 - chatVisible ? 0 : chatPanelWidth, 39 + chatVisible ? 0 : panelWidthWithInsets, 40 40 ); 41 - sidebarOffset.value = withSpring(chatVisible ? 0 : chatPanelWidth + 64, { 41 + sidebarOffset.value = withSpring(chatVisible ? 0 : panelWidthWithInsets, { 42 + damping: 100, 43 + stiffness: 1000, 44 + }); 45 + sidebarOpacity.value = withSpring(chatVisible ? 1 : 0, { 42 46 damping: 100, 43 47 stiffness: 1000, 44 48 }); 45 - }, [chatVisible, chatPanelWidth, sidebarOffset]); 49 + }, [chatVisible, panelWidthWithInsets, sidebarOffset]); 46 50 47 51 const animatedSidebarStyle = useAnimatedStyle(() => ({ 48 52 transform: [ 49 53 { translateX: sidebarOffset.value }, 50 54 { translateY: -kb.keyboardHeight }, 51 55 ], 56 + opacity: sidebarOpacity.value, 52 57 })); 53 58 54 59 return ( 55 - <Animated.View 56 - style={[ 57 - layout.position.absolute, 58 - position.right[0], 59 - { 60 - top: safeAreaInsets.top, 61 - bottom: safeAreaInsets.bottom, 62 - right: safeAreaInsets.right / 2, 63 - width: chatPanelWidth, 64 - backgroundColor: "rgba(0, 0, 0, 0.85)", 65 - borderLeftWidth: 1, 66 - borderLeftColor: "rgba(255, 255, 255, 0.1)", 67 - zIndex: 999, 68 - }, 69 - animatedSidebarStyle, 70 - ]} 71 - > 72 - <View style={{ flex: 1, position: "relative" }}> 73 - <ChatPanel /> 74 - </View> 75 - </Animated.View> 60 + <> 61 + <Animated.View 62 + style={[ 63 + { 64 + width: chatPanelWidth, 65 + flexShrink: 0, 66 + }, 67 + animatedSidebarStyle, 68 + ]} 69 + /> 70 + <Animated.View 71 + style={[ 72 + { 73 + position: "absolute", 74 + right: 0, 75 + // attempt to lessen the impact of the safe area inset on the chat panel? 76 + paddingRight: insets.right > 0 ? insets.right - 20 : 0, 77 + top: 0, 78 + bottom: 0, 79 + width: panelWidthWithInsets, 80 + flexShrink: 0, 81 + backgroundColor: "rgba(0, 0, 0, 0.85)", 82 + borderLeftWidth: 1, 83 + borderLeftColor: "rgba(255, 255, 255, 0.1)", 84 + zIndex: 999, 85 + }, 86 + animatedSidebarStyle, 87 + ]} 88 + > 89 + <View style={{ flex: 1, position: "relative" }}> 90 + <ChatPanel /> 91 + </View> 92 + </Animated.View> 93 + </> 76 94 ); 77 95 } 78 96
+1 -3
js/app/components/mobile/desktop-ui.tsx
··· 57 57 toggleGoLive, 58 58 } = useLivestreamInfo(); 59 59 const { width, height } = usePlayerDimensions(); 60 - const { safeAreaInsets, shouldShowFloatingMetrics } = useResponsiveLayout(); 60 + const { shouldShowFloatingMetrics } = useResponsiveLayout(); 61 61 62 62 const offline = useOffline(); 63 63 const showMetrics = usePlayerStore((state) => state.showDebugInfo); ··· 200 200 layout.position.absolute, 201 201 w.percent[100], 202 202 { 203 - top: safeAreaInsets.top, 204 203 paddingHorizontal: 16, 205 204 paddingVertical: 16, 206 205 }, ··· 213 212 ingest={ingest} 214 213 isChatOpen={isChatOpen || false} 215 214 onToggleChat={toggleChat} 216 - safeAreaInsets={safeAreaInsets} 217 215 embedded={embedded} 218 216 /> 219 217 </Animated.View>
-2
js/app/components/mobile/desktop-ui/top-controls.tsx
··· 28 28 ingest: string | null; 29 29 isChatOpen: boolean; 30 30 onToggleChat: () => void; 31 - safeAreaInsets: { top: number }; 32 31 embedded?: boolean; 33 32 } 34 33 ··· 38 37 ingest, 39 38 isChatOpen, 40 39 onToggleChat, 41 - safeAreaInsets, 42 40 embedded = false, 43 41 }: TopControlBarProps) { 44 42 const navigation = useNavigation();
+31 -38
js/app/components/mobile/player.tsx
··· 11 11 Text, 12 12 usePlayerDimensions, 13 13 usePlayerStore, 14 - useSegmentDimensions, 15 14 View, 16 15 } from "@streamplace/components"; 17 16 import { gap, h, pt, w } from "@streamplace/components/src/lib/theme/atoms"; ··· 20 19 import { ArrowLeft, ArrowRight } from "lucide-react-native"; 21 20 import { ComponentRef, useEffect, useRef, useState } from "react"; 22 21 import { Animated, Platform, ScrollView, StatusBar } from "react-native"; 22 + import { SafeAreaView } from "react-native-safe-area-context"; 23 23 import { useStore } from "store"; 24 24 import { useUserProfile } from "store/hooks"; 25 25 import { BottomMetadata } from "./bottom-metadata"; ··· 35 35 }, 36 36 ) { 37 37 const [showChat, setShowChat] = useState(true); 38 - const { shouldShowChatSidePanel, chatPanelWidth, safeAreaInsets } = 39 - useResponsiveLayout(); 38 + const { shouldShowChatSidePanel, chatPanelWidth } = useResponsiveLayout(); 40 39 const chatVisible = shouldShowChatSidePanel && showChat; 41 40 42 41 const [isStreamingElsewhere, setIsStreamingElsewhere] = useState< ··· 55 54 }, [userIsLive]); 56 55 57 56 const navigation = useNavigation(); 58 - const setSidebarHidden = useStore((state) => state.setSidebarHidden); 59 - const setSidebarUnhidden = useStore((state) => state.setSidebarUnhidden); 60 57 61 58 useEffect(() => { 62 59 return () => { ··· 134 131 flex: 1, 135 132 width: "100%", 136 133 height: "100%", 137 - paddingLeft: safeAreaInsets.left, 138 - paddingRight: safeAreaInsets.right, 139 134 }} 140 135 > 141 136 <PlayerInner ··· 147 142 <DesktopChatPanel 148 143 chatVisible={chatVisible} 149 144 chatPanelWidth={chatPanelWidth} 150 - safeAreaInsets={safeAreaInsets} 151 145 /> 152 146 ) : ( 153 147 <MobileUi /> ··· 174 168 screenWidth, 175 169 contentWidth, 176 170 availableHeight, 177 - safeAreaInsets, 178 171 } = useResponsiveLayout({ 179 172 sidebarWidth: sb.animatedWidth, 180 173 sidebarHidden: !sb.isActive, ··· 186 179 187 180 // content info 188 181 const { width, height } = usePlayerDimensions(); 189 - 190 - const { isPlayerRatioGreater } = useSegmentDimensions(); 191 182 192 183 // Calculate aspect ratio and determine if we're in desktop mode 193 184 const aspectRatio = width > 0 && height > 0 ? width / height : 16 / 9; ··· 255 246 maxHeight: "auto", 256 247 }, 257 248 { 258 - paddingTop: 259 - isPlayerRatioGreater && !isLandscape ? safeAreaInsets.top : 0, 249 + // paddingTop: 250 + // isPlayerRatioGreater && !isLandscape ? safeAreaInsets.top : 0, 260 251 }, 261 252 ]} 262 253 > 263 - <PlayerInnerInner {...props}> 264 - {showFullDesktopMode || fullscreen ? ( 265 - <DesktopUi dropdownPortalContainer={dropdownPortalRef.current} /> 266 - ) : ( 267 - isLandscape && ( 268 - <MobileUi 269 - setShowChat={props.setShowChat} 270 - showChat={props.showChat} 271 - /> 272 - ) 273 - )} 274 - <PlayerUI.ViewerLoadingOverlay /> 275 - <OfflineCounter isMobile={true} /> 276 - <View 277 - ref={dropdownPortalRef} 278 - style={{ 279 - position: "absolute", 280 - top: 0, 281 - left: 0, 282 - right: 0, 283 - bottom: 0, 284 - pointerEvents: "none", 285 - }} 286 - /> 287 - </PlayerInnerInner> 254 + <SafeAreaView edges={["left", "top"]} style={{ flex: 1 }}> 255 + <PlayerInnerInner {...props}> 256 + {showFullDesktopMode || fullscreen ? ( 257 + <DesktopUi dropdownPortalContainer={dropdownPortalRef.current} /> 258 + ) : ( 259 + isLandscape && ( 260 + <MobileUi 261 + setShowChat={props.setShowChat} 262 + showChat={props.showChat} 263 + /> 264 + ) 265 + )} 266 + <PlayerUI.ViewerLoadingOverlay /> 267 + <OfflineCounter isMobile={true} /> 268 + <View 269 + ref={dropdownPortalRef} 270 + style={{ 271 + position: "absolute", 272 + top: 0, 273 + left: 0, 274 + right: 0, 275 + bottom: 0, 276 + pointerEvents: "none", 277 + }} 278 + /> 279 + </PlayerInnerInner> 280 + </SafeAreaView> 288 281 </Animated.View> 289 282 {showFullDesktopMode && ( 290 283 <BottomMetadata
+58 -71
js/app/components/mobile/ui.tsx
··· 21 21 View, 22 22 zero, 23 23 } from "@streamplace/components"; 24 + import { px, py } from "@streamplace/components/src/ui"; 24 25 import { 25 26 ChevronLeft, 26 27 ChevronRight, ··· 40 41 useSharedValue, 41 42 withTiming, 42 43 } from "react-native-reanimated"; 44 + import { SafeAreaView } from "react-native-safe-area-context"; 43 45 import { MobileChatPanel } from "./chat"; 44 46 import { useResponsiveLayout } from "./useResponsiveLayout"; 45 47 ··· 77 79 const muted = useMuted(); 78 80 const setMuted = useSetMuted(); 79 81 80 - const { 81 - shouldShowFloatingMetrics, 82 - shouldShowChatSidePanel, 83 - chatPanelWidth, 84 - safeAreaInsets, 85 - } = useResponsiveLayout(); 82 + const { shouldShowFloatingMetrics, shouldShowChatSidePanel, chatPanelWidth } = 83 + useResponsiveLayout(); 86 84 87 85 const [showLoading, setShowLoading] = useState(false); 88 - 89 - // get width/height 90 - // showchat is a proxy for if we're in landscape or not :-( 91 - if (showChat != undefined) { 92 - safeAreaInsets.top = 0; 93 - } 94 86 95 87 useEffect(() => { 96 88 return () => { ··· 165 157 ]} 166 158 > 167 159 {/* Main UI Overlay */} 168 - <View 169 - style={[layout.position.absolute, h.percent[100], w.percent[100]]} 170 - > 171 - {/* Left Controls Column */} 172 - <LeftControlsPanel 173 - navigation={navigation} 174 - profile={profile} 175 - avatars={avatars} 176 - safeAreaInsets={safeAreaInsets} 177 - muted={muted} 178 - setMuted={setMuted} 179 - muteWasForced={muteWasForced} 180 - setMuteWasForced={setMuteWasForced} 181 - /> 182 - 183 - {/* Right Controls Column */} 184 - <View 160 + <View style={[h.percent[100], w.percent[100]]}> 161 + <SafeAreaView 162 + edges={["top"]} 185 163 style={[ 186 - layout.position.absolute, 187 - position.right[2], 188 - { top: safeAreaInsets.top + 12 }, 164 + px[2], 165 + py[2], 189 166 layout.flex.row, 190 - gap.all[2], 167 + layout.flex.spaceBetween, 168 + w.percent[100], 191 169 ]} 192 170 > 193 - {shouldShowFloatingMetrics && ( 194 - <View> 195 - <View 196 - style={[ 197 - { 198 - padding: 9, 199 - backgroundColor: "rgba(90,90,90, 0.3)", 200 - borderRadius: 12, 201 - }, 202 - r[2], 203 - ]} 204 - > 205 - <PlayerUI.Viewers /> 171 + {/* Left Controls Column */} 172 + <View 173 + style={[layout.flex.column, gap.all[2], { maxWidth: "70%" }]} 174 + > 175 + <LeftControlsPanel 176 + navigation={navigation} 177 + profile={profile} 178 + avatars={avatars} 179 + muted={muted} 180 + setMuted={setMuted} 181 + muteWasForced={muteWasForced} 182 + setMuteWasForced={setMuteWasForced} 183 + /> 184 + </View> 185 + 186 + {/* Right Controls Column */} 187 + <View 188 + style={[layout.flex.row, gap.all[2], layout.flex.align.start]} 189 + > 190 + {shouldShowFloatingMetrics && ( 191 + <View> 192 + <View 193 + style={[ 194 + { 195 + padding: 9, 196 + backgroundColor: "rgba(90,90,90, 0.3)", 197 + borderRadius: 12, 198 + }, 199 + r[2], 200 + ]} 201 + > 202 + <PlayerUI.Viewers /> 203 + </View> 206 204 </View> 207 - </View> 208 - )} 205 + )} 209 206 210 - <RightControlsPanel 211 - ingest={ingest} 212 - doSetIngestCamera={doSetIngestCamera} 213 - shouldShowChatSidePanel={shouldShowChatSidePanel} 214 - showChat={showChat} 215 - setShowChat={setShowChat} 216 - /> 217 - </View> 207 + <RightControlsPanel 208 + ingest={ingest} 209 + doSetIngestCamera={doSetIngestCamera} 210 + shouldShowChatSidePanel={shouldShowChatSidePanel} 211 + showChat={showChat} 212 + setShowChat={setShowChat} 213 + /> 214 + </View> 215 + </SafeAreaView> 218 216 219 217 {shouldShowFloatingMetrics && isLive && ( 220 218 <View 221 219 style={[ 222 220 layout.position.absolute, 223 - { top: safeAreaInsets.top + 112 }, 221 + position.top[28], 224 222 position.left[0], 225 223 position.right[0], 226 224 layout.flex.column, ··· 282 280 navigation, 283 281 profile, 284 282 avatars, 285 - safeAreaInsets, 286 283 muted, 287 284 setMuted, 288 285 muteWasForced, ··· 291 288 navigation: any; 292 289 profile: any; 293 290 avatars: any; 294 - safeAreaInsets: { top: number; bottom: number; left: number; right: number }; 295 291 muted: boolean; 296 292 setMuted: (muted: boolean) => void; 297 293 muteWasForced: boolean; ··· 303 299 (segment?.contentWarnings?.warnings as string[]) || []; 304 300 305 301 return ( 306 - <View 307 - style={[ 308 - layout.position.absolute, 309 - position.left[2], 310 - { top: safeAreaInsets.top + 12 }, 311 - layout.flex.column, 312 - gap.all[2], 313 - { maxWidth: "70%" }, 314 - ]} 315 - > 302 + <> 316 303 {/* Back Button and Profile */} 317 304 <View 318 305 style={[ ··· 401 388 <View> 402 389 <ContentWarningBadge warnings={contentWarnings} /> 403 390 </View> 404 - </View> 391 + </> 405 392 ); 406 393 } 407 394 ··· 458 445 { 459 446 backgroundColor: "rgba(90,90,90, 0.3)", 460 447 borderRadius: 12, 448 + paddingVertical: 2.25 * 4, 461 449 }, 462 450 zero.r[2], 463 451 showChat === undefined ··· 465 453 : zero.layout.flex.row, 466 454 zero.layout.flex.center, 467 455 zero.gap.all[4], 468 - zero.py[3], 469 456 showChat === undefined ? zero.px[2] : zero.px[3], 470 457 zero.layout.position.relative, 471 458 ]}
+2 -13
js/app/components/mobile/useResponsiveLayout.ts
··· 2 2 import { useMemo } from "react"; 3 3 import { useWindowDimensions } from "react-native"; 4 4 import { SharedValue } from "react-native-reanimated"; 5 - import { useSafeAreaInsets } from "react-native-safe-area-context"; 6 5 7 6 export interface ResponsiveLayoutConfig { 8 7 shouldShowChatSidePanel: boolean; 9 8 shouldShowFloatingMetrics: boolean; 10 9 chatPanelWidth: number; 11 - safeAreaInsets: { 12 - top: number; 13 - bottom: number; 14 - left: number; 15 - right: number; 16 - }; 17 10 screenWidth: number; 18 11 availableHeight: number; 19 12 } ··· 30 23 contentWidth: number; 31 24 } { 32 25 const { width: screenWidth, height: screenHeight } = useWindowDimensions(); 33 - const safeAreaInsets = useSafeAreaInsets(); 34 26 35 27 const sidebarWidthValue = useMemo(() => { 36 28 if (typeof sidebarWidth === "object" && "value" in sidebarWidth) { ··· 50 42 isLandscape && screenWidth >= 768 && showChatSidePanelOnLandscape; 51 43 52 44 const shouldShowFloatingMetrics = screenWidth < 768; 53 - const availableHeight = 54 - screenHeight - safeAreaInsets.top - safeAreaInsets.bottom; 45 + const availableHeight = screenHeight; 55 46 56 47 const chatPanelWidth = responsiveValue( 57 48 { ··· 63 54 screenWidth, 64 55 ); 65 56 66 - const availableWidth = 67 - screenWidth - safeAreaInsets.left - safeAreaInsets.right / 2; 57 + const availableWidth = screenWidth; 68 58 69 59 const contentWidth = 70 60 !sidebarHidden && sidebarWidthValue > 0 ··· 75 65 shouldShowChatSidePanel, 76 66 shouldShowFloatingMetrics, 77 67 chatPanelWidth, 78 - safeAreaInsets, 79 68 contentWidth, 80 69 screenWidth, 81 70 availableHeight,
+1 -1
js/components/src/components/ui/resizeable.tsx
··· 39 39 }: ResizableChatSheetProps) { 40 40 const { slideKeyboard } = useKeyboardSlide(); 41 41 const { bottom: safeBottom } = useSafeAreaInsets(); 42 - const MAX_HEIGHT = (SCREEN_HEIGHT - safeBottom) * 0.5; 42 + const MAX_HEIGHT = (SCREEN_HEIGHT - safeBottom) * 0.55; 43 43 const MIN_HEIGHT = -(SCREEN_HEIGHT - safeBottom) * 0.2; 44 44 const COLLAPSE_HEIGHT = (SCREEN_HEIGHT - safeBottom) * 0.1; 45 45