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) {
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};