Live video on the AT Protocol
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};