Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
32 kB · 970 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971import { BottomTabIcon, createBottomTabNavigator,} from "@react-navigation/bottom-tabs";import { useLinkTo, useNavigation } from "@react-navigation/native";import { createNativeStackNavigator, NativeStackHeaderBackProps, NativeStackNavigationOptions,} from "@react-navigation/native-stack";import { Text, useAccentColor, useBrandingSettled, useDID, usePrimaryColor, useStreamplaceStore, useTheme, withAlpha, zero,} from "@streamplace/components";import { colors, spacing } from "@streamplace/components/src/lib/theme/tokens";import { Settings } from "components";import { SiteTitleLockup, useNodeTitle } from "components/brand/logo";import { LogoBrandMenu } from "components/brand/logo-brand-menu";import Login from "components/login/login";import LoginModal from "components/login/login-modal";import PdsHostSelectorModal from "components/login/pds-host-selector-modal";import { MobileAppBanner } from "components/mobile-app-banner";import { AboutCategorySettings } from "components/settings/about-category-settings";import { AccountCategorySettings } from "components/settings/account-category-settings";import { AdvancedCategorySettings } from "components/settings/advanced-category-settings";import { BackupSettings } from "components/settings/backup-settings";import { BadgeIssuerPanel } from "components/settings/badge-issuer-panel";import { BadgeSelectionManager } from "components/settings/badge-selection-manager";import { BrandingAdmin } from "components/settings/branding-admin";import { DanmuCategorySettings } from "components/settings/danmu-category-settings";import KeyManager from "components/settings/key-manager";import { LanguagesCategorySettings } from "components/settings/languages-category-settings";import MultistreamManager from "components/settings/multistream-manager";import { NotificationsCategorySettings } from "components/settings/notifications-category-settings";import { PrivacyCategorySettings } from "components/settings/privacy-category-settings";import RecommendationsManager from "components/settings/recommendations-manager";import { StreamingCategorySettings } from "components/settings/streaming-category-settings";import WebhookManager from "components/settings/webhook-manager";import { SidebarOverlay, SidebarToggle,} from "components/sidebar/sidebar-overlay";import UploadProgressIndicator from "components/upload/upload-progress-indicator";import { useBlueskyNotifications } from "hooks/useBlueskyNotifications";import usePlatform from "hooks/usePlatform";import { useIsLargeScreen, useSidebarControl } from "hooks/useSidebarControl";import { Clapperboard, Cog, Home, Video } from "lucide-react-native";import { useEffect, useState } from "react";import { Platform, Pressable, StatusBar, View } from "react-native";import Animated, { useAnimatedStyle } from "react-native-reanimated";import { SFSymbols7_0 } from "sf-symbols-typescript";import "src/navigation-types";import AboutScreen from "src/screens/about";import AppReturnScreen from "src/screens/app-return";import PopoutChat from "src/screens/chat-popout";import DanmuOBSScreen from "src/screens/danmu-obs";import DownloadScreen from "src/screens/download";import EmbedScreen from "src/screens/embed";import HomeScreen from "src/screens/home";import InfoWidgetEmbed from "src/screens/info-widget-embed";import LaunchGoLive from "src/screens/launch-go-live";import LiveDashboard from "src/screens/live-dashboard";import MobileGoLive from "src/screens/mobile-go-live";import MobileStream from "src/screens/mobile-stream";import MultiScreen from "src/screens/multi";import PopoutInfoWidget from "src/screens/popout-info-widget";import PopoutLivestream from "src/screens/popout-livestream";import PopoutMultistream from "src/screens/popout-multistream";import PopoutStreamMonitor from "src/screens/popout-stream-monitor";import SupportScreen from "src/screens/support";import UploadScreen, { UploadDraftsScreen, UploadLivestreamsScreen, UploadVideoScreen, UploadVideosScreen,} from "src/screens/upload";import VideoScreen from "src/screens/video";import VideoListScreen from "src/screens/video-list";import VodScreen from "src/screens/vod";import VodEmbedScreen from "src/screens/vod-embed";import { useStore } from "store";import { useHydrated, useNotificationDestination, useNotificationToken,} from "store/hooks";import { AvatarButton, LGAvatarButton, NavigationButton, UploadButton,} from "./router";
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
const Tab = createBottomTabNavigator();const RootStack = createNativeStackNavigator();const HomeStack = createNativeStackNavigator();const VideosStack = createNativeStackNavigator();const SettingsStack = createNativeStackNavigator();
function useBaseScreenOptions() { const z = useTheme(); return { headerShown: true, headerTransparent: Platform.OS === "ios", headerBackButtonDisplayMode: "minimal" as const, // Slim, quiet chrome: 15px medium title on surface0 with a hairline headerTitleStyle: { fontFamily: z.theme.typography.universal.xl.fontFamily, fontSize: 15, fontWeight: "500" as const, color: z.theme.colors.text1, }, headerStyle: { backgroundColor: Platform.OS === "web" ? z.theme.colors.surface0 : undefined, borderBottomColor: z.theme.colors.borderSubtle, borderBottomWidth: 1, // Slimmer bar, aligned with the 56px sidebar brand row. height: 56, }, };}
// Home navigator (contains home + all general navigation screens)function HomeNavigator() { const title = useNodeTitle(); const baseScreenOptions = useBaseScreenOptions(); const isNative = Platform.OS !== "web"; const z = useTheme(); const did = useDID();
const headerScreenOptions: NativeStackNavigationOptions = { headerShown: !isNative, headerLeft: isNative ? undefined : ({ canGoBack }: NativeStackHeaderBackProps) => ( <NavigationButton canGoBack={canGoBack} /> ), headerRight: () => ( <View style={{ flexDirection: "row", alignItems: "center" }}> <UploadButton /> <LGAvatarButton /> </View> ), ...(isNative && { headerTransparent: true, }), ...(Platform.OS === "ios" && { unstable_headerRightItems: () => [ { type: "custom", hidesSharedBackground: true, element: ( <View style={{ flexDirection: "row", alignItems: "center" }}> <UploadButton /> <LGAvatarButton /> </View> ), }, ], }), headerTitleStyle: { fontFamily: z.theme.typography.universal.xl.fontFamily, fontSize: 15, fontWeight: "500" as const, color: z.theme.colors.text1, }, };
return ( <HomeStack.Navigator screenOptions={baseScreenOptions}> <HomeStack.Screen name="HomeMain" component={HomeScreen} options={{ // `title` drives the browser tab: the node's title (runtime // branding, else the brand default). The visible web header shows // the contextual "Home", not a repeat of it. title, headerTitle: Platform.OS === "ios" ? (props) => ( <View style={{ flex: 1, alignItems: "flex-start" }}> <Text size="2xl" style={[zero.ml[4]]}> {title} </Text> </View> ) : "Home", headerLeft: Platform.OS !== "ios" ? ({ canGoBack }) => <NavigationButton canGoBack={canGoBack} /> : undefined, headerRight: () => ( <View style={{ flexDirection: "row", alignItems: "center" }}> <UploadButton /> <AvatarButton /> </View> ), ...(Platform.OS === "ios" && { unstable_headerRightItems: () => [ { type: "custom", hidesSharedBackground: true, element: ( <View style={{ flexDirection: "row", alignItems: "center" }}> <UploadButton /> <LGAvatarButton /> </View> ), }, ], }), }} /> <HomeStack.Screen name="About" component={AboutScreen} options={{ title: "What's Streamplace?", ...headerScreenOptions, }} /> <HomeStack.Screen name="Download" component={DownloadScreen} options={{ title: "Download", ...headerScreenOptions }} /> <HomeStack.Screen name="LiveDashboard" component={LiveDashboard} options={{ title: "Live streaming", ...headerScreenOptions }} /> <HomeStack.Screen name="Login" component={Login} options={{ title: did ? "Account" : "Login", ...headerScreenOptions }} /> <HomeStack.Screen name="Multi" component={MultiScreen} options={{ title: "Multi-stream", ...headerScreenOptions }} /> <HomeStack.Screen name="Support" component={SupportScreen} options={{ title: "Support", ...headerScreenOptions }} /> {!isNative && ( <HomeStack.Screen name="Upload" component={UploadScreen} options={{ title: "Upload Video", ...headerScreenOptions }} /> )} {!isNative && ( <HomeStack.Screen name="UploadVideo" component={UploadVideoScreen} options={{ title: "Edit Video", ...headerScreenOptions }} /> )} {!isNative && ( <HomeStack.Screen name="UploadDrafts" component={UploadDraftsScreen} options={{ title: "Drafts", ...headerScreenOptions }} /> )} {!isNative && ( <HomeStack.Screen name="UploadLivestreams" component={UploadLivestreamsScreen} options={{ title: "Livestreams", ...headerScreenOptions }} /> )} {!isNative && ( <HomeStack.Screen name="UploadVideos" component={UploadVideosScreen} options={{ title: "My Videos", ...headerScreenOptions }} /> )} </HomeStack.Navigator> );}
// Videos stack navigator (global + per-user VOD listings). Unlike the pushed// Home screens, these are tab roots, so they always carry their own header.function VideosNavigator() { const baseScreenOptions = useBaseScreenOptions(); const isNative = Platform.OS !== "web";
return ( <VideosStack.Navigator screenOptions={{ ...baseScreenOptions, headerLeft: isNative ? undefined : ({ canGoBack }: NativeStackHeaderBackProps) => ( <NavigationButton canGoBack={canGoBack} /> ), headerRight: () => ( <View style={{ flexDirection: "row", alignItems: "center" }}> <UploadButton /> <LGAvatarButton /> </View> ), ...(Platform.OS === "ios" && { unstable_headerRightItems: () => [ { type: "custom", hidesSharedBackground: true, element: ( <View style={{ flexDirection: "row", alignItems: "center" }}> <UploadButton /> <LGAvatarButton /> </View> ), }, ], }), }} > <VideosStack.Screen name="VideoList" component={VideoListScreen} options={{ title: "Videos" }} /> <VideosStack.Screen name="UserVideoList" component={VideoListScreen} options={{ title: "Videos" }} /> </VideosStack.Navigator> );}
// Settings stack navigatorfunction SettingsNavigator() { const baseScreenOptions = useBaseScreenOptions(); const headerScreenOptions: NativeStackNavigationOptions = { ...baseScreenOptions, headerTransparent: Platform.OS === "ios", headerBackButtonDisplayMode: "minimal" as const, headerShown: true, // Same Create + avatar cluster as the other navigators, so the header // controls don't vanish on Settings screens. headerRight: () => ( <View style={{ flexDirection: "row", alignItems: "center" }}> <UploadButton /> <LGAvatarButton /> </View> ), ...(Platform.OS === "ios" && { unstable_headerRightItems: () => [ { type: "custom", hidesSharedBackground: true, element: ( <View style={{ flexDirection: "row", alignItems: "center" }}> <UploadButton /> <LGAvatarButton /> </View> ), }, ], }), }; return ( <SettingsStack.Navigator initialRouteName="MainSettings" screenOptions={{ ...headerScreenOptions, }} > <SettingsStack.Screen name="MainSettings" component={Settings} options={{ title: "Settings" }} /> <SettingsStack.Screen name="AboutCategory" component={AboutCategorySettings} options={{ title: "About" }} /> <SettingsStack.Screen name="AccountCategory" component={AccountCategorySettings} options={{ title: "Account" }} /> <SettingsStack.Screen name="StreamingCategory" component={StreamingCategorySettings} options={{ title: "Streaming" }} /> <SettingsStack.Screen name="WebhooksSettings" component={WebhookManager} options={{ title: "Webhooks" }} /> <SettingsStack.Screen name="BackupSettings" component={BackupSettings} options={{ title: "Backup" }} /> <SettingsStack.Screen name="RecommendationsSettings" component={RecommendationsManager} options={{ title: "Recommendations" }} /> <SettingsStack.Screen name="PrivacyCategory" component={PrivacyCategorySettings} options={{ title: "Privacy & Security" }} /> <SettingsStack.Screen name="NotificationsCategory" component={NotificationsCategorySettings} options={{ title: "Notifications" }} /> <SettingsStack.Screen name="DanmuCategory" component={DanmuCategorySettings} options={{ title: "Danmu" }} /> <SettingsStack.Screen name="AdvancedCategory" component={AdvancedCategorySettings} options={{ title: "Advanced" }} /> <SettingsStack.Screen name="MultistreamCategory" component={MultistreamManager} options={{ title: "Multistream" }} /> <SettingsStack.Screen name="LanguagesCategory" component={LanguagesCategorySettings} options={{ title: "Languages" }} /> <SettingsStack.Screen name="KeyManagement" component={KeyManager} options={{ title: "Key Manager" }} /> <SettingsStack.Screen name="BadgeSelection" component={BadgeSelectionManager} options={{ title: "Badges" }} /> <SettingsStack.Screen name="BadgeIssuer" component={BadgeIssuerPanel} options={{ title: "Issue Badges" }} /> <SettingsStack.Screen name="BrandingAdmin" component={BrandingAdmin} options={{ title: "Branding" }} /> </SettingsStack.Navigator> );}
const IOS_ICONS: Record<string, SFSymbols7_0> = { Home: "house.fill", Videos: "play.rectangle.fill", Settings: "gearshape.fill",};const ANDROID_ICONS = { Home: "home", Videos: "video_library", Settings: "settings",};
const getIcon = ( name: keyof typeof IOS_ICONS | keyof typeof ANDROID_ICONS,): BottomTabIcon => { if (Platform.OS === "ios") { return { type: "sfSymbol", name: IOS_ICONS[name], }; } else { return { type: "materialSymbol", name: ANDROID_ICONS[name], }; }};
// Tab navigator (main app sections, navigation on web is handled in sidebar)function TabNavigator() { const { isNative, isBrowser } = usePlatform(); const accentColor = useAccentColor(); const primaryColor = usePrimaryColor(); const isLargeScreen = useIsLargeScreen(); const z = useTheme();
return ( <Tab.Navigator screenOptions={{ lazy: true, headerShown: false, // Hide tab bar on web and < 800px tabBarStyle: isNative ? { backgroundColor: z.theme.colors.surface1, borderTopColor: z.theme.colors.borderSubtle, } : !isLargeScreen ? { backgroundColor: z.theme.colors.surface1, borderTopColor: z.theme.colors.borderSubtle, } : { display: "none" }, tabBarActiveTintColor: accentColor || primaryColor || z.theme.colors.primary, tabBarInactiveTintColor: z.theme.colors.text3, headerTitleStyle: { fontFamily: z.theme.typography.universal["2xl"].fontFamily, }, headerStyle: { backgroundColor: z.theme.colors.background, }, }} > <Tab.Screen name="HomeTab" component={HomeNavigator} options={{ title: "Home", ...(isNative ? { tabBarIcon: getIcon("Home"), } : { tabBarIcon: ({ color, size }) => ( <Home size={size} color={color} /> ), }), }} /> <Tab.Screen name="VideosTab" component={VideosNavigator} options={{ title: "Videos", ...(isNative ? { tabBarIcon: getIcon("Videos"), } : { tabBarIcon: ({ color, size }) => ( <Clapperboard size={size} color={color} /> ), }), }} /> {!isNative && ( <Tab.Screen name="GoLiveTab" component={LaunchGoLive} options={{ title: "Go Live", tabBarIcon: ({ color, size }) => ( <Video size={size} color={color} /> ), headerShown: true, headerTransparent: true, }} /> )} <Tab.Screen name="SettingsTab" component={SettingsNavigator} options={{ title: "Settings", ...(isNative ? { tabBarIcon: getIcon("Settings"), } : { tabBarIcon: ({ color, size }) => ( <Cog size={size} color={color} /> ), }), headerShown: false, }} /> </Tab.Navigator> );}
// Walk the (possibly nested) navigation state to the deepest active route, so// checks against leaf screen names like "LiveDashboard" resolve correctly —// getState().routes[index].name alone only yields the top-level route// (e.g. "MainTabs"), so nested screens never match.function getActiveLeafRouteName(state: any): string | undefined { if (!state?.routes) return undefined; const route = state.routes[state.index ?? 0]; if (route?.state) return getActiveLeafRouteName(route.state) ?? route.name; return route?.name;}
export default function Shell() { const { isNative } = usePlatform(); const sidebar = useSidebarControl(); const navigation = useNavigation(); const hydrate = useStore((state) => state.hydrate); const initPushNotifications = useStore( (state) => state.initPushNotifications, ); const registerNotificationToken = useStore( (state) => state.registerNotificationToken, ); const clearNotification = useStore((state) => state.clearNotification); const pollMySegments = useStore((state) => state.pollMySegments); const showLoginModal = useStore((state) => state.showLoginModal); const closeLoginModal = useStore((state) => state.closeLoginModal); const showPdsModal = useStore((state) => state.showPdsModal); const openPdsModal = useStore((state) => state.openPdsModal); const closePdsModal = useStore((state) => state.closePdsModal); const loginAction = useStore((state) => state.login); const openLoginLink = useStore((state) => state.openLoginLink); const z = useTheme(); const baseScreenOptions = useBaseScreenOptions();
// Top-level hydration and initialization useEffect(() => { hydrate(); initPushNotifications(); }, []);
const notificationToken = useNotificationToken(); const did = useStore((state) => state.oauthSession?.did); const hydrated = useHydrated();
// Two more things the first frame waits for (bounded by the same timeout): // the OAuth session restore, so a signed-in viewer never sees the wall // that the anonymous status answer would paint; and branding, so the // default mark and title never flash before the node's own. const sessionRestoring = useStreamplaceStore( (s) => s.oauthSession === undefined, ); const brandingSettled = useBrandingSettled(); const [bootTimedOut, setBootTimedOut] = useState(false); useEffect(() => { const handle = setTimeout(() => setBootTimedOut(true), 6000); return () => clearTimeout(handle); }, []); // Re-register when the token changes OR once the logged-in DID resolves, so a // token acquired before the OAuth session finishes restoring still gets its // repoDID association registered (otherwise the user is excluded from // follower livestream notifications). useEffect(() => { if (notificationToken) { registerNotificationToken(); } }, [notificationToken, did]);
// Handle incoming push notification routing const notificationDestination = useNotificationDestination(); const linkTo = useLinkTo();
useEffect(() => { if (notificationDestination) { linkTo(notificationDestination); clearNotification(); } }, [notificationDestination]);
// Poll for live streamers useEffect(() => { let handle: NodeJS.Timeout; handle = setInterval(() => { pollMySegments(); }, 2500); pollMySegments(); return () => clearInterval(handle); }, []);
useBlueskyNotifications();
// Track current route const [currentRouteName, setCurrentRouteName] = useState<string | undefined>( () => { try { return getActiveLeafRouteName(navigation.getState()); } catch { return undefined; } }, );
useEffect(() => { const update = () => { const name = getActiveLeafRouteName(navigation.getState()); if (name) setCurrentRouteName(name); }; update(); // seed on mount so direct loads (e.g. a stream URL) resolve now const unsubscribe = navigation.addListener("state", update); return unsubscribe; }, [navigation]);
// Detail views (watching a stream or video) take the sidebar out of the flow // so it opens as an overlay drawer over dimmed content instead of pushing. const setOverlay = useStore((state) => state.setOverlay); const closeDrawer = useStore((state) => state.closeDrawer); // A video page reports itself while mounted (the front door showing the // default video is one the route name doesn't reveal). const videoPage = useStore((state) => state.videoPage); const isDetailView = currentRouteName === "Stream" || currentRouteName === "Video" || currentRouteName === "Vod" || videoPage; // Video pages get a YouTube-style sticky translucent header the content // scrolls under; the livestream keeps its own solid header. const isVodDetail = currentRouteName === "Video" || currentRouteName === "Vod" || videoPage; useEffect(() => { setOverlay(isDetailView); }, [isDetailView, setOverlay]);
// Animate content margin when sidebar is active (web only). In overlay mode // the content stays full width (margin 0) and the drawer floats over it. const animatedContentStyle = useAnimatedStyle(() => { if (isNative || !sidebar.isActive) { return { marginLeft: 0 }; } return { marginLeft: sidebar.animatedContentMargin.value, }; });
// Scrim that dims content behind the overlay drawer. const scrimStyle = useAnimatedStyle(() => ({ opacity: sidebar.animatedScrim.value, }));
if (!hydrated) { return <View />; }
if ((sessionRestoring || !brandingSettled) && !bootTimedOut) { return <View />; }
return ( <View style={{ flex: 1 }}> <StatusBar barStyle="light-content" /> {!isNative && <SidebarOverlay />} {!isNative && <MobileAppBanner />} <Animated.View style={[{ flex: 1 }, animatedContentStyle]}> <RootStack.Navigator screenOptions={{ // Reuse the shared chrome (surface0 + hairline, 15px title) so // pushed screens like Stream don't render a differently-colored // header than the HomeStack. ...baseScreenOptions, headerShown: !isNative, headerLeft: ({ canGoBack }) => ( <NavigationButton canGoBack={canGoBack} /> ), headerRight: () => ( <View style={{ flexDirection: "row", alignItems: "center" }}> <UploadButton /> <LGAvatarButton /> </View> ), ...(isNative && { headerTransparent: true, }), }} > {/* Main tabs (initial screen for all platforms) */} <RootStack.Screen name="MainTabs" component={TabNavigator} options={{ headerShown: false }} />
{/* Full-screen screens that should NOT have tab bar accessible on mobile */} <RootStack.Screen name="Stream" component={MobileStream} options={{ headerShown: Platform.OS === "web", headerTitle: "", }} /> <RootStack.Screen name="MobileGoLive" component={MobileGoLive} options={{ headerShown: false }} /> <RootStack.Screen name="Video" component={VideoScreen} options={{ headerShown: false }} /> <RootStack.Screen name="Vod" component={VodScreen} options={{ headerShown: false }} />
{/* Utility/embed screens */} <RootStack.Screen name="AppReturn" component={AppReturnScreen} options={{ title: "Returning to app..." }} /> <RootStack.Screen name="PopoutChat" component={PopoutChat} options={{ headerShown: false }} /> <RootStack.Screen name="Embed" component={EmbedScreen} options={{ headerShown: false }} /> <RootStack.Screen name="VodEmbed" component={VodEmbedScreen} options={{ headerShown: false }} /> <RootStack.Screen name="InfoWidgetEmbed" component={InfoWidgetEmbed} options={{ headerShown: false }} /> <RootStack.Screen name="DanmuOBS" component={DanmuOBSScreen} options={{ headerShown: false }} /> <RootStack.Screen name="PopoutStreamMonitor" component={PopoutStreamMonitor} options={{ headerShown: false }} /> <RootStack.Screen name="PopoutInfoWidget" component={PopoutInfoWidget} options={{ headerShown: false }} /> <RootStack.Screen name="PopoutMultistream" component={PopoutMultistream} options={{ headerShown: false }} /> <RootStack.Screen name="PopoutLivestream" component={PopoutLivestream} options={{ headerShown: false }} /> </RootStack.Navigator> </Animated.View> {/* Scrim behind the overlay drawer (detail views only) */} {!isNative && ( <AnimatedPressable accessibilityLabel="Close menu" pointerEvents={ sidebar.overlay && sidebar.drawerOpen ? "auto" : "none" } onPress={closeDrawer} style={[ scrimStyle, { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#000", // token-ok: overlay scrim zIndex: 127000, }, ]} /> )} {/* Header cluster on detail views: hamburger + logo, sitting exactly where the drawer's brand row will appear so it stays put as the drawer opens. */} {!isNative && sidebar.isActive && sidebar.overlay && !sidebar.drawerOpen && ( <View style={[ { position: "absolute", top: 0, height: 56, flexDirection: "row", alignItems: "center", // Match the sidebar brand row (gap 0) so the logo sits in the // same spot when navigating between the sidebar and detail-view // headers. gap: 0, zIndex: 128001, }, isVodDetail ? // Full-width liquid-glass bar the video scrolls under. ({ left: 0, right: 0, paddingHorizontal: spacing[2], backgroundColor: withAlpha(z.theme.colors.surface0, 0.55), backdropFilter: "blur(18px)", WebkitBackdropFilter: "blur(18px)", borderBottomWidth: 1, borderBottomColor: z.theme.colors.borderSubtle, } as any) : { left: spacing[2] }, ]} > <SidebarToggle label="Open menu" onPress={sidebar.toggle} /> <LogoBrandMenu> <Pressable // @ts-ignore renders as <a> on web href="/" style={{ flexShrink: 1, minWidth: 0 }} onPress={(e: any) => { e?.preventDefault?.(); navigation.navigate("MainTabs" as any, { screen: "HomeTab", params: { screen: "HomeMain" }, }); }} > <SiteTitleLockup size={19} weight="semibold" letterSpacing={0} markColor={colors.white} color={colors.white} /> </Pressable> </LogoBrandMenu> </View> )} <LoginModal visible={showLoginModal} onClose={closeLoginModal} onOpenPdsModal={openPdsModal} /> <PdsHostSelectorModal open={showPdsModal} onOpenChange={closePdsModal} onSubmit={(pdsHost) => { closePdsModal(); loginAction(pdsHost, openLoginLink); }} /> <UploadProgressIndicator /> </View> );}