Live video on the AT Protocol
at natb/command-errors 943 lines 28 kB view raw
1import "@expo/metro-runtime"; 2import { 3 createDrawerNavigator, 4 DrawerContentScrollView, 5 DrawerItem, 6 DrawerItemList, 7} from "@react-navigation/drawer"; 8import { 9 CommonActions, 10 DrawerActions, 11 LinkingOptions, 12 NavigatorScreenParams, 13 useLinkTo, 14 useNavigation, 15 useRoute, 16} from "@react-navigation/native"; 17import { createNativeStackNavigator } from "@react-navigation/native-stack"; 18import { 19 Button, 20 Text, 21 useDefaultStreamer, 22 useSiteTitle, 23 useTheme, 24 useToast, 25 zero, 26} from "@streamplace/components"; 27import { Provider, Settings } from "components"; 28import AQLink from "components/aqlink"; 29import Login from "components/login/login"; 30import LoginModal from "components/login/login-modal"; 31import { AboutCategorySettings } from "components/settings/about-category-settings"; 32import { AccountCategorySettings } from "components/settings/account-category-settings"; 33import { AdvancedCategorySettings } from "components/settings/advanced-category-settings"; 34import { DanmuCategorySettings } from "components/settings/danmu-category-settings"; 35import { PrivacyCategorySettings } from "components/settings/privacy-category-settings"; 36import { StreamingCategorySettings } from "components/settings/streaming-category-settings"; 37import WebhookManager from "components/settings/webhook-manager"; 38import Sidebar, { ExternalDrawerItem } from "components/sidebar/sidebar"; 39import * as ExpoLinking from "expo-linking"; 40import { useLiveUser } from "hooks/useLiveUser"; 41import usePlatform from "hooks/usePlatform"; 42import { useSidebarControl } from "hooks/useSidebarControl"; 43import { 44 ArrowLeft, 45 Book, 46 Download, 47 ExternalLink, 48 Home, 49 LogIn, 50 Menu, 51 PanelLeftClose, 52 PanelLeftOpen, 53 Settings as SettingsIcon, 54 ShieldQuestion, 55 User, 56 Video, 57} from "lucide-react-native"; 58import React, { Fragment, useEffect, useState } from "react"; 59import { 60 ImageBackground, 61 ImageSourcePropType, 62 Linking, 63 Platform, 64 Pressable, 65 StatusBar, 66 useWindowDimensions, 67 View, 68} from "react-native"; 69import AboutScreen from "./screens/about"; 70import AppReturnScreen from "./screens/app-return"; 71import PopoutChat from "./screens/chat-popout"; 72import DownloadScreen from "./screens/download"; 73import EmbedScreen from "./screens/embed"; 74import InfoWidgetEmbed from "./screens/info-widget-embed"; 75import LiveDashboard from "./screens/live-dashboard"; 76import MultiScreen from "./screens/multi"; 77import SupportScreen from "./screens/support"; 78 79import KeyManager from "components/settings/key-manager"; 80 81import HomeScreen from "./screens/home"; 82 83import { useUrl } from "@streamplace/components"; 84import PdsHostSelectorModal from "components/login/pds-host-selector-modal"; 85import { BrandingAdmin } from "components/settings/branding-admin"; 86import { LanguagesCategorySettings } from "components/settings/languages-category-settings"; 87import MultistreamManager from "components/settings/multistream-manager"; 88import RecommendationsManager from "components/settings/recommendations-manager"; 89import Constants from "expo-constants"; 90import { useBlueskyNotifications } from "hooks/useBlueskyNotifications"; 91import { SystemBars } from "react-native-edge-to-edge"; 92import { 93 configureReanimatedLogger, 94 ReanimatedLogLevel, 95 useAnimatedStyle, 96} from "react-native-reanimated"; 97import { useStore } from "store"; 98import { 99 useHydrated, 100 useNotificationDestination, 101 useNotificationToken, 102 useUserProfile, 103} from "store/hooks"; 104import DanmuOBSScreen from "./screens/danmu-obs"; 105import MobileGoLive from "./screens/mobile-go-live"; 106import MobileStream from "./screens/mobile-stream"; 107 108// Initialize sidebar state on app load 109useStore.getState().loadStateFromStorage(); 110 111const Stack = createNativeStackNavigator(); 112 113// disabled strict b/c chat swipeable triggers it a LOT and the resulting logging 114// slows down the whole app 115configureReanimatedLogger({ 116 level: ReanimatedLogLevel.warn, 117 strict: false, 118}); 119 120type HomeStackParamList = { 121 StreamList: undefined; 122 Stream: { user: string }; 123}; 124 125type SettingsStackParamList = { 126 MainSettings: undefined; 127 AboutCategory: undefined; 128 AccountCategory: undefined; 129 StreamingCategory: undefined; 130 WebhooksSettings: undefined; 131 RecommendationsSettings: undefined; 132 PrivacyCategory: undefined; 133 DanmuCategory: undefined; 134 AdvancedCategory: undefined; 135 LanguagesCategory: undefined; 136 DeveloperSettings: undefined; 137 KeyManagement: undefined; 138 MultistreamCategory: undefined; 139 BrandingAdmin: undefined; 140}; 141 142type RootStackParamList = { 143 Home: NavigatorScreenParams<HomeStackParamList>; 144 Multi: { config: string }; 145 Support: undefined; 146 Settings: NavigatorScreenParams<SettingsStackParamList>; 147 KeyManagement: undefined; 148 GoLive: undefined; 149 LiveDashboard: undefined; 150 Login: undefined; 151 AVSync: undefined; 152 AppReturn: { scheme: string }; 153 About: undefined; 154 Download: undefined; 155 PopoutChat: { user: string }; 156 Embed: { user: string }; 157 InfoWidgetEmbed: undefined; 158 LegacyStream: { user: string }; 159 DanmuOBS: { user: string }; 160 MobileGoLive: undefined; 161}; 162 163declare global { 164 namespace ReactNavigation { 165 interface RootParamList extends RootStackParamList {} 166 } 167} 168 169const linking: LinkingOptions<ReactNavigation.RootParamList> = { 170 prefixes: [ExpoLinking.createURL("")], 171 config: { 172 screens: { 173 Home: { 174 screens: { 175 StreamList: "", 176 Stream: { 177 path: ":user", 178 }, 179 }, 180 }, 181 Multi: "multi/:config", 182 Support: "support", 183 Settings: { 184 screens: { 185 MainSettings: "settings", 186 AboutCategory: "settings/about", 187 AccountCategory: "settings/account", 188 StreamingCategory: "settings/streaming", 189 WebhooksSettings: "settings/streaming/webhooks", 190 RecommendationsSettings: "settings/streaming/recommendations", 191 PrivacyCategory: "settings/privacy", 192 DanmuCategory: "settings/danmu", 193 AdvancedCategory: "settings/advanced", 194 DeveloperSettings: "settings/developer", 195 MultistreamCategory: "settings/streaming/multistream", 196 KeyManagement: "settings/streaming/key-management", 197 LanguagesCategory: "settings/languages", 198 BrandingAdmin: "settings/branding", 199 }, 200 }, 201 KeyManagement: "key-management", 202 GoLive: "golive", 203 LiveDashboard: "live", 204 Login: "login", 205 AVSync: "sync-test", 206 AppReturn: "app-return/:scheme", 207 About: "about", 208 Download: "download", 209 PopoutChat: "chat-popout/:user", 210 Embed: "embed/:user", 211 InfoWidgetEmbed: "info-widget", 212 LegacyStream: "legacy/:user", 213 DanmuOBS: "widgets/:user/danmu", 214 MobileGoLive: "mobile-golive", 215 }, 216 }, 217}; 218 219const associatedDomain = Constants.expoConfig?.ios?.associatedDomains?.[0]; 220if (associatedDomain && associatedDomain.startsWith("applinks:")) { 221 const domain = associatedDomain.slice("applinks:".length); 222 linking.prefixes.push(`https://${domain}`); 223} 224 225// https://github.com/streamplace/streamplace/issues/377 226const hasDevDomain = linking.prefixes.some((prefix) => 227 prefix.includes("tv.aquareum.dev"), 228); 229if (hasDevDomain) { 230 linking.prefixes.push("tv.aquareum://"); 231 linking.prefixes.push("https://stream.place"); 232} 233 234console.log("Linking prefixes", linking.prefixes); 235 236const Drawer = createDrawerNavigator(); 237 238const NavigationButton = ({ canGoBack }: { canGoBack?: boolean }) => { 239 const sidebar = useSidebarControl(); 240 const navigation = useNavigation(); 241 const { theme } = useTheme(); 242 243 const handlePress = () => { 244 if (sidebar?.isActive) { 245 sidebar.toggle(); 246 } 247 }; 248 249 const handleGoBackPress = () => { 250 if (canGoBack) { 251 navigation.goBack(); 252 } else { 253 navigation.dispatch(DrawerActions.toggleDrawer()); 254 } 255 }; 256 257 return ( 258 <View 259 style={[ 260 { flexDirection: "row" }, 261 { 262 marginLeft: Platform.OS === "android" ? 0 : 12, 263 marginRight: Platform.OS === "android" ? 12 : 0, 264 }, 265 ]} 266 > 267 {sidebar?.isActive ? ( 268 <> 269 <Pressable style={{ padding: 5 }} onPress={handlePress}> 270 {sidebar.isCollapsed ? ( 271 <PanelLeftOpen size={24} color={theme.colors.accentForeground} /> 272 ) : ( 273 <PanelLeftClose size={24} color={theme.colors.accentForeground} /> 274 )} 275 </Pressable> 276 {canGoBack && ( 277 <Pressable 278 style={{ marginLeft: 10, paddingVertical: 5 }} 279 onPress={handleGoBackPress} 280 > 281 <ArrowLeft size={24} color={theme.colors.accentForeground} /> 282 </Pressable> 283 )} 284 </> 285 ) : ( 286 <Pressable style={{ padding: 5 }} onPress={handleGoBackPress}> 287 {canGoBack ? ( 288 <ArrowLeft size={24} color={theme.colors.accentForeground} /> 289 ) : ( 290 <Menu size={24} color={theme.colors.accentForeground} /> 291 )} 292 </Pressable> 293 )} 294 </View> 295 ); 296}; 297 298const AvatarButton = () => { 299 const userProfile = useUserProfile(); 300 const openLoginModal = useStore((state) => state.openLoginModal); 301 const openPDSModal = useStore((state) => state.openPdsModal); 302 const loginAction = useStore((state) => state.login); 303 const openLoginLink = useStore((state) => state.openLoginLink); 304 const { theme } = useTheme(); 305 let source: ImageSourcePropType | undefined = undefined; 306 307 const windowWidth = useWindowDimensions().width; 308 309 const isCompact = windowWidth <= 800; 310 311 if (userProfile) { 312 source = { uri: userProfile.avatar }; 313 return ( 314 <AQLink 315 to={{ screen: "Settings", params: { screen: "AccountCategory" } }} 316 > 317 <ImageBackground 318 key={source?.uri ?? "default"} 319 source={source} 320 style={{ 321 width: 40, 322 height: 40, 323 borderRadius: 24, 324 overflow: "hidden", 325 marginRight: 10, 326 backgroundColor: "black", 327 justifyContent: "center", 328 alignItems: "center", 329 }} 330 > 331 <User size={24} color="white" style={{ zIndex: -2 }} /> 332 </ImageBackground> 333 </AQLink> 334 ); 335 } 336 337 if (isCompact) { 338 return ( 339 <Button 340 onPress={() => openLoginModal()} 341 variant="ghost" 342 size="icon" 343 width="min" 344 style={{ marginRight: 10, marginLeft: "auto" }} 345 > 346 <LogIn size={20} color={theme.colors.text} /> 347 </Button> 348 ); 349 } 350 351 return ( 352 <View 353 style={{ 354 flexDirection: "row", 355 alignItems: "center", 356 gap: 8, 357 marginRight: 10, 358 }} 359 > 360 <Button 361 onPress={() => openLoginModal()} 362 variant="secondary" 363 width="min" 364 style={[zero.r.full]} 365 > 366 <Text style={{ color: theme.colors.text }}>Log In</Text> 367 </Button> 368 <Button 369 onPress={() => openPDSModal()} 370 variant="primary" 371 width="min" 372 style={[zero.r.full]} 373 > 374 <Text style={{ color: theme.colors.text }}>Sign Up</Text> 375 </Button> 376 <Button 377 width="min" 378 size="icon" 379 variant="secondary" 380 style={[zero.r.full]} 381 onPress={() => openLoginModal()} 382 > 383 <User size={24} color="white" /> 384 </Button> 385 </View> 386 ); 387}; 388 389const useExternalItems = (): ExternalDrawerItem[] => { 390 const streamplaceUrl = useUrl(); 391 const { theme } = useTheme(); 392 const defaultStreamer = useDefaultStreamer(); 393 394 if (defaultStreamer) { 395 return []; 396 } 397 398 return [ 399 { 400 item: React.memo(() => <Book size={24} color={theme.colors.text} />), 401 label: ( 402 <Text variant="h5" style={{ alignSelf: "flex-start" }}> 403 Documentation{" "} 404 <ExternalLink 405 size={16} 406 color={theme.colors.mutedForeground} 407 style={{ 408 position: "relative", 409 top: 2, 410 }} 411 /> 412 </Text> 413 ) as any, 414 onPress: () => { 415 const u = new URL(streamplaceUrl); 416 u.pathname = "/docs"; 417 Linking.openURL(u.toString()); 418 }, 419 }, 420 ]; 421}; 422 423// TODO: merge in ^ 424function CustomDrawerContent(props) { 425 let { theme } = useTheme(); 426 return ( 427 <DrawerContentScrollView {...props}> 428 <DrawerItemList {...props} /> 429 <DrawerItem 430 icon={() => <Book size={24} color={theme.colors.text} />} 431 label={() => ( 432 <Text style={{ alignSelf: "flex-start" }}> 433 Documentation{" "} 434 <ExternalLink 435 size={16} 436 color="#666" 437 style={{ 438 position: "relative", 439 top: 2, 440 }} 441 /> 442 </Text> 443 )} 444 onPress={() => { 445 const u = new URL(window.location.href); 446 u.pathname = "/docs"; 447 Linking.openURL(u.toString()); 448 }} 449 /> 450 </DrawerContentScrollView> 451 ); 452} 453 454export default function Router() { 455 return ( 456 <Provider linking={linking}> 457 <StreamplaceDrawer /> 458 </Provider> 459 ); 460} 461 462export function StreamplaceDrawer() { 463 const theme = useTheme(); 464 const { isWeb, isElectron, isNative, isBrowser } = usePlatform(); 465 const navigation = useNavigation(); 466 const hydrate = useStore((state) => state.hydrate); 467 const initPushNotifications = useStore( 468 (state) => state.initPushNotifications, 469 ); 470 const registerNotificationToken = useStore( 471 (state) => state.registerNotificationToken, 472 ); 473 const clearNotification = useStore((state) => state.clearNotification); 474 const pollMySegments = useStore((state) => state.pollMySegments); 475 const showLoginModal = useStore((state) => state.showLoginModal); 476 const closeLoginModal = useStore((state) => state.closeLoginModal); 477 const showPdsModal = useStore((state) => state.showPdsModal); 478 const openPdsModal = useStore((state) => state.openPdsModal); 479 const closePdsModal = useStore((state) => state.closePdsModal); 480 const [livePopup, setLivePopup] = useState(false); 481 const loginAction = useStore((state) => state.login); 482 const openLoginLink = useStore((state) => state.openLoginLink); 483 const siteTitle = useSiteTitle(); 484 const defaultStreamer = useDefaultStreamer(); 485 486 const sidebar = useSidebarControl(); 487 488 const toast = useToast(); 489 490 SystemBars.setStyle("dark"); 491 492 // Top-level stuff to handle push notification registration 493 useEffect(() => { 494 hydrate(); 495 initPushNotifications(); 496 }, []); 497 const notificationToken = useNotificationToken(); 498 const userProfile = useUserProfile(); 499 const hydrated = useHydrated(); 500 501 // check if current user is the default streamer 502 const isDefaultStreamer = 503 defaultStreamer && userProfile?.did === defaultStreamer; 504 useEffect(() => { 505 if (notificationToken) { 506 registerNotificationToken(); 507 } 508 }, [notificationToken, userProfile]); 509 510 // Stuff to handle incoming push notification routing 511 const notificationDestination = useNotificationDestination(); 512 const linkTo = useLinkTo(); 513 514 const animatedDrawerStyle = useAnimatedStyle(() => { 515 return { 516 width: sidebar.isActive ? sidebar.animatedWidth.value : undefined, 517 }; 518 }); 519 520 useEffect(() => { 521 if (notificationDestination) { 522 linkTo(notificationDestination); 523 clearNotification(); 524 } 525 }, [notificationDestination]); 526 527 // Top-level stuff to handle polling for live streamers 528 useEffect(() => { 529 let handle: NodeJS.Timeout; 530 handle = setInterval(() => { 531 pollMySegments(); 532 }, 2500); 533 pollMySegments(); 534 return () => clearInterval(handle); 535 }, []); 536 537 const userIsLive = useLiveUser(); 538 useBlueskyNotifications(); 539 540 let foregroundColor = theme.theme.colors.text || "#fff"; 541 542 // are we in the live dashboard? 543 const [isLiveDashboard, setIsLiveDashboard] = useState(false); 544 useEffect(() => { 545 if (!isLiveDashboard && userIsLive) { 546 toast.show("You are live!", "Do you want to go to your Live Dashboard?", { 547 actionLabel: "Go", 548 onAction: () => { 549 navigation.navigate("LiveDashboard"); 550 setLivePopup(false); 551 }, 552 onClose: () => setLivePopup(false), 553 variant: "error", 554 duration: 8, 555 }); 556 } 557 }, [userIsLive]); 558 const externalItems = useExternalItems(); 559 560 if (!hydrated) { 561 return <View />; 562 } 563 564 return ( 565 <> 566 <StatusBar barStyle="light-content" /> 567 <Drawer.Navigator 568 initialRouteName="Home" 569 screenOptions={{ 570 // for the custom sidebar 571 drawerType: sidebar.isActive ? "permanent" : "front", 572 swipeEnabled: !sidebar.isActive, 573 drawerStyle: [ 574 { 575 zIndex: 128000, 576 }, 577 sidebar.isActive ? animatedDrawerStyle : [], 578 ], 579 // rest 580 headerLeft: () => ( 581 <> 582 {/* this is a hack to give the popup the navigator context */} 583 <PopupChecker setIsLiveDashboard={setIsLiveDashboard} /> 584 <NavigationButton /> 585 </> 586 ), 587 headerRight: () => <AvatarButton />, 588 drawerActiveTintColor: "#a0287c33", 589 unmountOnBlur: true, 590 }} 591 drawerContent={ 592 sidebar.isActive 593 ? (props) => ( 594 <Sidebar 595 {...props} 596 collapsed={sidebar.isCollapsed} 597 hidden={sidebar.isHidden} 598 widthAnim={sidebar.animatedWidth} 599 externalItems={externalItems} 600 /> 601 ) 602 : CustomDrawerContent 603 } 604 > 605 <Drawer.Screen 606 name="Home" 607 component={MainTab} 608 options={{ 609 drawerIcon: () => <Home color={foregroundColor} size={24} />, 610 drawerLabel: () => <Text variant="h5">Home</Text>, 611 headerTitle: isWeb ? "Home" : siteTitle, 612 headerShown: isWeb, 613 title: siteTitle, 614 }} 615 listeners={{ 616 drawerItemPress: (e) => { 617 e.preventDefault(); 618 navigation.dispatch( 619 CommonActions.reset({ 620 index: 0, 621 routes: [ 622 { 623 name: "Home", 624 state: { 625 routes: [{ name: "StreamList" }], 626 }, 627 }, 628 ], 629 }), 630 ); 631 }, 632 }} 633 /> 634 <Drawer.Screen 635 name="About" 636 component={AboutScreen} 637 options={{ 638 drawerLabel: () => <Text variant="h5">What's Streamplace?</Text>, 639 drawerIcon: () => ( 640 <ShieldQuestion color={foregroundColor} size={24} /> 641 ), 642 drawerItemStyle: 643 isNative || defaultStreamer ? { display: "none" } : undefined, 644 }} 645 /> 646 <Drawer.Screen 647 name="Download" 648 component={DownloadScreen} 649 options={{ 650 drawerLabel: () => <Text variant="h5">Download</Text>, 651 drawerIcon: () => <Download color={foregroundColor} size={24} />, 652 drawerItemStyle: 653 !isBrowser || defaultStreamer ? { display: "none" } : undefined, 654 }} 655 /> 656 <Drawer.Screen 657 name="Settings" 658 component={SettingsStack} 659 options={{ 660 drawerIcon: () => ( 661 <SettingsIcon color={foregroundColor} size={24} /> 662 ), 663 drawerLabel: () => <Text variant="h5">Settings</Text>, 664 headerShown: false, 665 }} 666 listeners={{ 667 drawerItemPress: (e) => { 668 e.preventDefault(); 669 navigation.dispatch( 670 CommonActions.reset({ 671 index: 0, 672 routes: [ 673 { 674 name: "Settings", 675 }, 676 ], 677 }), 678 ); 679 }, 680 }} 681 /> 682 <Drawer.Screen 683 name="KeyManagement" 684 component={KeyManager} 685 options={{ 686 drawerLabel: () => <Text variant="h5">Key Manager</Text>, 687 drawerItemStyle: { display: "none" }, 688 }} 689 /> 690 <Drawer.Screen 691 name="Support" 692 component={SupportScreen} 693 options={{ 694 drawerLabel: () => <Text variant="h5">Support</Text>, 695 drawerItemStyle: { display: "none" }, 696 }} 697 /> 698 <Drawer.Screen 699 name="LiveDashboard" 700 component={LiveDashboard} 701 options={{ 702 drawerLabel: () => <Text variant="h5">Live Dashboard</Text>, 703 drawerIcon: () => <Video color={foregroundColor} size={24} />, 704 drawerItemStyle: 705 isNative || (defaultStreamer && !isDefaultStreamer) 706 ? { display: "none" } 707 : undefined, 708 }} 709 /> 710 <Drawer.Screen 711 name="AppReturn" 712 component={AppReturnScreen} 713 options={{ 714 drawerLabel: () => null, 715 drawerItemStyle: { display: "none" }, 716 headerShown: false, 717 }} 718 /> 719 <Drawer.Screen 720 name="Multi" 721 component={MultiScreen} 722 options={{ 723 drawerLabel: () => null, 724 drawerItemStyle: { display: "none" }, 725 }} 726 /> 727 <Drawer.Screen 728 name="Login" 729 component={Login} 730 options={{ 731 drawerLabel: () => null, 732 drawerItemStyle: { display: "none" }, 733 headerShown: false, 734 }} 735 /> 736 <Drawer.Screen 737 name="PopoutChat" 738 component={PopoutChat} 739 options={{ 740 drawerLabel: () => null, 741 drawerItemStyle: { display: "none" }, 742 headerShown: false, 743 drawerStyle: { display: "none" }, 744 }} 745 /> 746 <Drawer.Screen 747 name="Embed" 748 component={EmbedScreen} 749 options={{ 750 drawerLabel: () => null, 751 drawerItemStyle: { display: "none" }, 752 headerShown: false, 753 }} 754 /> 755 <Drawer.Screen 756 name="InfoWidgetEmbed" 757 component={InfoWidgetEmbed} 758 options={{ 759 drawerLabel: () => null, 760 drawerItemStyle: { display: "none" }, 761 headerShown: false, 762 }} 763 /> 764 <Drawer.Screen 765 name="DanmuOBS" 766 component={DanmuOBSScreen} 767 options={{ 768 drawerLabel: () => null, 769 drawerItemStyle: { display: "none" }, 770 headerShown: false, 771 }} 772 /> 773 <Drawer.Screen 774 name="MobileGoLive" 775 component={MobileGoLive} 776 options={{ 777 headerTitle: "Go Live", 778 drawerItemStyle: 779 !isNative || (defaultStreamer && !isDefaultStreamer) 780 ? { display: "none" } 781 : undefined, 782 drawerLabel: () => <Text variant="h5">Go Live</Text>, 783 title: "Go live", 784 drawerIcon: () => <Video color={foregroundColor} size={24} />, 785 headerShown: false, 786 }} 787 /> 788 </Drawer.Navigator> 789 <LoginModal 790 visible={showLoginModal} 791 onClose={closeLoginModal} 792 onOpenPdsModal={openPdsModal} 793 /> 794 <PdsHostSelectorModal 795 open={showPdsModal} 796 onOpenChange={closePdsModal} 797 onSubmit={(pdsHost) => { 798 closePdsModal(); 799 loginAction(pdsHost, openLoginLink); 800 }} 801 /> 802 </> 803 ); 804} 805 806export const PopupChecker = ({ 807 setIsLiveDashboard, 808}: { 809 setIsLiveDashboard: (isLiveDashboard: boolean) => void; 810}) => { 811 const route = useRoute(); 812 useEffect(() => { 813 if (route.name === "LiveDashboard") { 814 setIsLiveDashboard(true); 815 } else { 816 setIsLiveDashboard(false); 817 } 818 }, [route.name]); 819 return <Fragment />; 820}; 821 822const MainTab = () => { 823 const { isWeb } = usePlatform(); 824 const siteTitle = useSiteTitle(); 825 const defaultStreamer = useDefaultStreamer(); 826 827 return ( 828 <Stack.Navigator 829 initialRouteName="StreamList" 830 screenOptions={{ 831 headerLeft: ({ canGoBack }) => ( 832 <NavigationButton canGoBack={canGoBack} /> 833 ), 834 headerRight: () => <AvatarButton />, 835 headerShown: !isWeb, 836 }} 837 > 838 <Stack.Screen 839 name="StreamList" 840 component={ 841 defaultStreamer && defaultStreamer !== "" ? MobileStream : HomeScreen 842 } 843 options={{ headerTitle: siteTitle, title: siteTitle }} 844 /> 845 <Stack.Screen 846 name="Stream" 847 component={MobileStream} 848 options={{ 849 headerTitle: "Stream", 850 title: "Streamplace Stream", 851 headerShown: false, 852 }} 853 /> 854 </Stack.Navigator> 855 ); 856}; 857 858const SettingsStack = () => { 859 const { isWeb } = usePlatform(); 860 return ( 861 <Stack.Navigator 862 initialRouteName="MainSettings" 863 screenOptions={{ 864 headerLeft: ({ canGoBack }) => ( 865 <NavigationButton canGoBack={canGoBack} /> 866 ), 867 headerRight: () => <AvatarButton />, 868 }} 869 > 870 <Stack.Screen 871 name="MainSettings" 872 component={Settings} 873 options={{ headerTitle: "Settings", title: "Settings" }} 874 /> 875 <Stack.Screen 876 name="AboutCategory" 877 component={AboutCategorySettings} 878 options={{ headerTitle: "About", title: "About" }} 879 /> 880 <Stack.Screen 881 name="AccountCategory" 882 component={AccountCategorySettings} 883 options={{ headerTitle: "Account", title: "Account" }} 884 /> 885 <Stack.Screen 886 name="StreamingCategory" 887 component={StreamingCategorySettings} 888 options={{ headerTitle: "Streaming", title: "Streaming" }} 889 /> 890 <Stack.Screen 891 name="WebhooksSettings" 892 component={WebhookManager} 893 options={{ headerTitle: "Webhooks", title: "Webhooks" }} 894 /> 895 <Stack.Screen 896 name="RecommendationsSettings" 897 component={RecommendationsManager} 898 options={{ headerTitle: "Recommendations", title: "Recommendations" }} 899 /> 900 <Stack.Screen 901 name="PrivacyCategory" 902 component={PrivacyCategorySettings} 903 options={{ 904 headerTitle: "Privacy & Security", 905 title: "Privacy & Security", 906 }} 907 /> 908 <Stack.Screen 909 name="DanmuCategory" 910 component={DanmuCategorySettings} 911 options={{ headerTitle: "Danmu", title: "Danmu" }} 912 /> 913 <Stack.Screen 914 name="AdvancedCategory" 915 component={AdvancedCategorySettings} 916 options={{ headerTitle: "Advanced", title: "Advanced" }} 917 /> 918 <Stack.Screen 919 name="LanguagesCategory" 920 component={LanguagesCategorySettings} 921 options={{ headerTitle: "Languages", title: "Languages" }} 922 /> 923 <Stack.Screen 924 name="KeyManagement" 925 component={KeyManager} 926 options={{ headerTitle: "Key Manager", title: "Key Manager" }} 927 /> 928 <Stack.Screen 929 name="MultistreamCategory" 930 component={MultistreamManager} 931 options={{ headerTitle: "Multistream", title: "Multistream" }} 932 /> 933 <Drawer.Screen 934 name="BrandingAdmin" 935 component={BrandingAdmin} 936 options={{ 937 drawerLabel: () => <Text variant="h5">Branding Admin</Text>, 938 drawerItemStyle: { display: "none" }, 939 }} 940 /> 941 </Stack.Navigator> 942 ); 943};