Live video on the AT Protocol
at next 947 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 && isWeb) { 546 toast.show( 547 "You are streaming!", 548 "Do you want to go to your Live Dashboard?", 549 { 550 actionLabel: "Go", 551 onAction: () => { 552 navigation.navigate("LiveDashboard"); 553 setLivePopup(false); 554 }, 555 onClose: () => setLivePopup(false), 556 variant: "error", 557 duration: 8, 558 }, 559 ); 560 } 561 }, [userIsLive]); 562 const externalItems = useExternalItems(); 563 564 if (!hydrated) { 565 return <View />; 566 } 567 568 return ( 569 <> 570 <StatusBar barStyle="light-content" /> 571 <Drawer.Navigator 572 initialRouteName="Home" 573 screenOptions={{ 574 // for the custom sidebar 575 drawerType: sidebar.isActive ? "permanent" : "front", 576 swipeEnabled: !sidebar.isActive, 577 drawerStyle: [ 578 { 579 zIndex: 128000, 580 }, 581 sidebar.isActive ? animatedDrawerStyle : [], 582 ], 583 // rest 584 headerLeft: () => ( 585 <> 586 {/* this is a hack to give the popup the navigator context */} 587 <PopupChecker setIsLiveDashboard={setIsLiveDashboard} /> 588 <NavigationButton /> 589 </> 590 ), 591 headerRight: () => <AvatarButton />, 592 drawerActiveTintColor: "#a0287c33", 593 unmountOnBlur: true, 594 }} 595 drawerContent={ 596 sidebar.isActive 597 ? (props) => ( 598 <Sidebar 599 {...props} 600 collapsed={sidebar.isCollapsed} 601 hidden={sidebar.isHidden} 602 widthAnim={sidebar.animatedWidth} 603 externalItems={externalItems} 604 /> 605 ) 606 : CustomDrawerContent 607 } 608 > 609 <Drawer.Screen 610 name="Home" 611 component={MainTab} 612 options={{ 613 drawerIcon: () => <Home color={foregroundColor} size={24} />, 614 drawerLabel: () => <Text variant="h5">Home</Text>, 615 headerTitle: isWeb ? "Home" : siteTitle, 616 headerShown: isWeb, 617 title: siteTitle, 618 }} 619 listeners={{ 620 drawerItemPress: (e) => { 621 e.preventDefault(); 622 navigation.dispatch( 623 CommonActions.reset({ 624 index: 0, 625 routes: [ 626 { 627 name: "Home", 628 state: { 629 routes: [{ name: "StreamList" }], 630 }, 631 }, 632 ], 633 }), 634 ); 635 }, 636 }} 637 /> 638 <Drawer.Screen 639 name="About" 640 component={AboutScreen} 641 options={{ 642 drawerLabel: () => <Text variant="h5">What's Streamplace?</Text>, 643 drawerIcon: () => ( 644 <ShieldQuestion color={foregroundColor} size={24} /> 645 ), 646 drawerItemStyle: 647 isNative || defaultStreamer ? { display: "none" } : undefined, 648 }} 649 /> 650 <Drawer.Screen 651 name="Download" 652 component={DownloadScreen} 653 options={{ 654 drawerLabel: () => <Text variant="h5">Download</Text>, 655 drawerIcon: () => <Download color={foregroundColor} size={24} />, 656 drawerItemStyle: 657 !isBrowser || defaultStreamer ? { display: "none" } : undefined, 658 }} 659 /> 660 <Drawer.Screen 661 name="Settings" 662 component={SettingsStack} 663 options={{ 664 drawerIcon: () => ( 665 <SettingsIcon color={foregroundColor} size={24} /> 666 ), 667 drawerLabel: () => <Text variant="h5">Settings</Text>, 668 headerShown: false, 669 }} 670 listeners={{ 671 drawerItemPress: (e) => { 672 e.preventDefault(); 673 navigation.dispatch( 674 CommonActions.reset({ 675 index: 0, 676 routes: [ 677 { 678 name: "Settings", 679 }, 680 ], 681 }), 682 ); 683 }, 684 }} 685 /> 686 <Drawer.Screen 687 name="KeyManagement" 688 component={KeyManager} 689 options={{ 690 drawerLabel: () => <Text variant="h5">Key Manager</Text>, 691 drawerItemStyle: { display: "none" }, 692 }} 693 /> 694 <Drawer.Screen 695 name="Support" 696 component={SupportScreen} 697 options={{ 698 drawerLabel: () => <Text variant="h5">Support</Text>, 699 drawerItemStyle: { display: "none" }, 700 }} 701 /> 702 <Drawer.Screen 703 name="LiveDashboard" 704 component={LiveDashboard} 705 options={{ 706 drawerLabel: () => <Text variant="h5">Live Dashboard</Text>, 707 drawerIcon: () => <Video color={foregroundColor} size={24} />, 708 drawerItemStyle: 709 isNative || (defaultStreamer && !isDefaultStreamer) 710 ? { display: "none" } 711 : undefined, 712 }} 713 /> 714 <Drawer.Screen 715 name="AppReturn" 716 component={AppReturnScreen} 717 options={{ 718 drawerLabel: () => null, 719 drawerItemStyle: { display: "none" }, 720 headerShown: false, 721 }} 722 /> 723 <Drawer.Screen 724 name="Multi" 725 component={MultiScreen} 726 options={{ 727 drawerLabel: () => null, 728 drawerItemStyle: { display: "none" }, 729 }} 730 /> 731 <Drawer.Screen 732 name="Login" 733 component={Login} 734 options={{ 735 drawerLabel: () => null, 736 drawerItemStyle: { display: "none" }, 737 headerShown: false, 738 }} 739 /> 740 <Drawer.Screen 741 name="PopoutChat" 742 component={PopoutChat} 743 options={{ 744 drawerLabel: () => null, 745 drawerItemStyle: { display: "none" }, 746 headerShown: false, 747 drawerStyle: { display: "none" }, 748 }} 749 /> 750 <Drawer.Screen 751 name="Embed" 752 component={EmbedScreen} 753 options={{ 754 drawerLabel: () => null, 755 drawerItemStyle: { display: "none" }, 756 headerShown: false, 757 }} 758 /> 759 <Drawer.Screen 760 name="InfoWidgetEmbed" 761 component={InfoWidgetEmbed} 762 options={{ 763 drawerLabel: () => null, 764 drawerItemStyle: { display: "none" }, 765 headerShown: false, 766 }} 767 /> 768 <Drawer.Screen 769 name="DanmuOBS" 770 component={DanmuOBSScreen} 771 options={{ 772 drawerLabel: () => null, 773 drawerItemStyle: { display: "none" }, 774 headerShown: false, 775 }} 776 /> 777 <Drawer.Screen 778 name="MobileGoLive" 779 component={MobileGoLive} 780 options={{ 781 headerTitle: "Go Live", 782 drawerItemStyle: 783 !isNative || (defaultStreamer && !isDefaultStreamer) 784 ? { display: "none" } 785 : undefined, 786 drawerLabel: () => <Text variant="h5">Go Live</Text>, 787 title: "Go live", 788 drawerIcon: () => <Video color={foregroundColor} size={24} />, 789 headerShown: false, 790 }} 791 /> 792 </Drawer.Navigator> 793 <LoginModal 794 visible={showLoginModal} 795 onClose={closeLoginModal} 796 onOpenPdsModal={openPdsModal} 797 /> 798 <PdsHostSelectorModal 799 open={showPdsModal} 800 onOpenChange={closePdsModal} 801 onSubmit={(pdsHost) => { 802 closePdsModal(); 803 loginAction(pdsHost, openLoginLink); 804 }} 805 /> 806 </> 807 ); 808} 809 810export const PopupChecker = ({ 811 setIsLiveDashboard, 812}: { 813 setIsLiveDashboard: (isLiveDashboard: boolean) => void; 814}) => { 815 const route = useRoute(); 816 useEffect(() => { 817 if (route.name === "LiveDashboard") { 818 setIsLiveDashboard(true); 819 } else { 820 setIsLiveDashboard(false); 821 } 822 }, [route.name]); 823 return <Fragment />; 824}; 825 826const MainTab = () => { 827 const { isWeb } = usePlatform(); 828 const siteTitle = useSiteTitle(); 829 const defaultStreamer = useDefaultStreamer(); 830 831 return ( 832 <Stack.Navigator 833 initialRouteName="StreamList" 834 screenOptions={{ 835 headerLeft: ({ canGoBack }) => ( 836 <NavigationButton canGoBack={canGoBack} /> 837 ), 838 headerRight: () => <AvatarButton />, 839 headerShown: !isWeb, 840 }} 841 > 842 <Stack.Screen 843 name="StreamList" 844 component={ 845 defaultStreamer && defaultStreamer !== "" ? MobileStream : HomeScreen 846 } 847 options={{ headerTitle: siteTitle, title: siteTitle }} 848 /> 849 <Stack.Screen 850 name="Stream" 851 component={MobileStream} 852 options={{ 853 headerTitle: "Stream", 854 title: "Streamplace Stream", 855 headerShown: false, 856 }} 857 /> 858 </Stack.Navigator> 859 ); 860}; 861 862const SettingsStack = () => { 863 const { isWeb } = usePlatform(); 864 return ( 865 <Stack.Navigator 866 initialRouteName="MainSettings" 867 screenOptions={{ 868 headerLeft: ({ canGoBack }) => ( 869 <NavigationButton canGoBack={canGoBack} /> 870 ), 871 headerRight: () => <AvatarButton />, 872 }} 873 > 874 <Stack.Screen 875 name="MainSettings" 876 component={Settings} 877 options={{ headerTitle: "Settings", title: "Settings" }} 878 /> 879 <Stack.Screen 880 name="AboutCategory" 881 component={AboutCategorySettings} 882 options={{ headerTitle: "About", title: "About" }} 883 /> 884 <Stack.Screen 885 name="AccountCategory" 886 component={AccountCategorySettings} 887 options={{ headerTitle: "Account", title: "Account" }} 888 /> 889 <Stack.Screen 890 name="StreamingCategory" 891 component={StreamingCategorySettings} 892 options={{ headerTitle: "Streaming", title: "Streaming" }} 893 /> 894 <Stack.Screen 895 name="WebhooksSettings" 896 component={WebhookManager} 897 options={{ headerTitle: "Webhooks", title: "Webhooks" }} 898 /> 899 <Stack.Screen 900 name="RecommendationsSettings" 901 component={RecommendationsManager} 902 options={{ headerTitle: "Recommendations", title: "Recommendations" }} 903 /> 904 <Stack.Screen 905 name="PrivacyCategory" 906 component={PrivacyCategorySettings} 907 options={{ 908 headerTitle: "Privacy & Security", 909 title: "Privacy & Security", 910 }} 911 /> 912 <Stack.Screen 913 name="DanmuCategory" 914 component={DanmuCategorySettings} 915 options={{ headerTitle: "Danmu", title: "Danmu" }} 916 /> 917 <Stack.Screen 918 name="AdvancedCategory" 919 component={AdvancedCategorySettings} 920 options={{ headerTitle: "Advanced", title: "Advanced" }} 921 /> 922 <Stack.Screen 923 name="LanguagesCategory" 924 component={LanguagesCategorySettings} 925 options={{ headerTitle: "Languages", title: "Languages" }} 926 /> 927 <Stack.Screen 928 name="KeyManagement" 929 component={KeyManager} 930 options={{ headerTitle: "Key Manager", title: "Key Manager" }} 931 /> 932 <Stack.Screen 933 name="MultistreamCategory" 934 component={MultistreamManager} 935 options={{ headerTitle: "Multistream", title: "Multistream" }} 936 /> 937 <Drawer.Screen 938 name="BrandingAdmin" 939 component={BrandingAdmin} 940 options={{ 941 drawerLabel: () => <Text variant="h5">Branding Admin</Text>, 942 drawerItemStyle: { display: "none" }, 943 }} 944 /> 945 </Stack.Navigator> 946 ); 947};