import "@expo/metro-runtime"; import { createDrawerNavigator, DrawerContentScrollView, DrawerItem, DrawerItemList, } from "@react-navigation/drawer"; import { CommonActions, DrawerActions, LinkingOptions, NavigatorScreenParams, useLinkTo, useNavigation, useRoute, } from "@react-navigation/native"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { Button, Text, useDefaultStreamer, useSiteTitle, useTheme, useToast, zero, } from "@streamplace/components"; import { Provider, Settings } from "components"; import AQLink from "components/aqlink"; import Login from "components/login/login"; import LoginModal from "components/login/login-modal"; import { AboutCategorySettings } from "components/settings/about-category-settings"; import { AccountCategorySettings } from "components/settings/account-category-settings"; import { AdvancedCategorySettings } from "components/settings/advanced-category-settings"; import { DanmuCategorySettings } from "components/settings/danmu-category-settings"; import { PrivacyCategorySettings } from "components/settings/privacy-category-settings"; import { StreamingCategorySettings } from "components/settings/streaming-category-settings"; import WebhookManager from "components/settings/webhook-manager"; import Sidebar, { ExternalDrawerItem } from "components/sidebar/sidebar"; import * as ExpoLinking from "expo-linking"; import { useLiveUser } from "hooks/useLiveUser"; import usePlatform from "hooks/usePlatform"; import { useSidebarControl } from "hooks/useSidebarControl"; import { ArrowLeft, Book, Download, ExternalLink, Home, LogIn, Menu, PanelLeftClose, PanelLeftOpen, Settings as SettingsIcon, ShieldQuestion, User, Video, } from "lucide-react-native"; import React, { Fragment, useEffect, useState } from "react"; import { ImageBackground, ImageSourcePropType, Linking, Platform, Pressable, StatusBar, useWindowDimensions, View, } from "react-native"; import AboutScreen from "./screens/about"; import AppReturnScreen from "./screens/app-return"; import PopoutChat from "./screens/chat-popout"; import DownloadScreen from "./screens/download"; import EmbedScreen from "./screens/embed"; import InfoWidgetEmbed from "./screens/info-widget-embed"; import LiveDashboard from "./screens/live-dashboard"; import MultiScreen from "./screens/multi"; import SupportScreen from "./screens/support"; import KeyManager from "components/settings/key-manager"; import HomeScreen from "./screens/home"; import { useUrl } from "@streamplace/components"; import PdsHostSelectorModal from "components/login/pds-host-selector-modal"; import { BrandingAdmin } from "components/settings/branding-admin"; import { LanguagesCategorySettings } from "components/settings/languages-category-settings"; import MultistreamManager from "components/settings/multistream-manager"; import RecommendationsManager from "components/settings/recommendations-manager"; import Constants from "expo-constants"; import { useBlueskyNotifications } from "hooks/useBlueskyNotifications"; import { SystemBars } from "react-native-edge-to-edge"; import { configureReanimatedLogger, ReanimatedLogLevel, useAnimatedStyle, } from "react-native-reanimated"; import { useStore } from "store"; import { useHydrated, useNotificationDestination, useNotificationToken, useUserProfile, } from "store/hooks"; import DanmuOBSScreen from "./screens/danmu-obs"; import MobileGoLive from "./screens/mobile-go-live"; import MobileStream from "./screens/mobile-stream"; // Initialize sidebar state on app load useStore.getState().loadStateFromStorage(); const Stack = createNativeStackNavigator(); // disabled strict b/c chat swipeable triggers it a LOT and the resulting logging // slows down the whole app configureReanimatedLogger({ level: ReanimatedLogLevel.warn, strict: false, }); type HomeStackParamList = { StreamList: undefined; Stream: { user: string }; }; type SettingsStackParamList = { MainSettings: undefined; AboutCategory: undefined; AccountCategory: undefined; StreamingCategory: undefined; WebhooksSettings: undefined; RecommendationsSettings: undefined; PrivacyCategory: undefined; DanmuCategory: undefined; AdvancedCategory: undefined; LanguagesCategory: undefined; DeveloperSettings: undefined; KeyManagement: undefined; MultistreamCategory: undefined; BrandingAdmin: undefined; }; type RootStackParamList = { Home: NavigatorScreenParams; Multi: { config: string }; Support: undefined; Settings: NavigatorScreenParams; KeyManagement: undefined; GoLive: undefined; LiveDashboard: undefined; Login: undefined; AVSync: undefined; AppReturn: { scheme: string }; About: undefined; Download: undefined; PopoutChat: { user: string }; Embed: { user: string }; InfoWidgetEmbed: undefined; LegacyStream: { user: string }; DanmuOBS: { user: string }; MobileGoLive: undefined; }; declare global { namespace ReactNavigation { interface RootParamList extends RootStackParamList {} } } const linking: LinkingOptions = { prefixes: [ExpoLinking.createURL("")], config: { screens: { Home: { screens: { StreamList: "", Stream: { path: ":user", }, }, }, Multi: "multi/:config", Support: "support", Settings: { screens: { MainSettings: "settings", AboutCategory: "settings/about", AccountCategory: "settings/account", StreamingCategory: "settings/streaming", WebhooksSettings: "settings/streaming/webhooks", RecommendationsSettings: "settings/streaming/recommendations", PrivacyCategory: "settings/privacy", DanmuCategory: "settings/danmu", AdvancedCategory: "settings/advanced", DeveloperSettings: "settings/developer", MultistreamCategory: "settings/streaming/multistream", KeyManagement: "settings/streaming/key-management", LanguagesCategory: "settings/languages", BrandingAdmin: "settings/branding", }, }, KeyManagement: "key-management", GoLive: "golive", LiveDashboard: "live", Login: "login", AVSync: "sync-test", AppReturn: "app-return/:scheme", About: "about", Download: "download", PopoutChat: "chat-popout/:user", Embed: "embed/:user", InfoWidgetEmbed: "info-widget", LegacyStream: "legacy/:user", DanmuOBS: "widgets/:user/danmu", MobileGoLive: "mobile-golive", }, }, }; const associatedDomain = Constants.expoConfig?.ios?.associatedDomains?.[0]; if (associatedDomain && associatedDomain.startsWith("applinks:")) { const domain = associatedDomain.slice("applinks:".length); linking.prefixes.push(`https://${domain}`); } // https://github.com/streamplace/streamplace/issues/377 const hasDevDomain = linking.prefixes.some((prefix) => prefix.includes("tv.aquareum.dev"), ); if (hasDevDomain) { linking.prefixes.push("tv.aquareum://"); linking.prefixes.push("https://stream.place"); } console.log("Linking prefixes", linking.prefixes); const Drawer = createDrawerNavigator(); const NavigationButton = ({ canGoBack }: { canGoBack?: boolean }) => { const sidebar = useSidebarControl(); const navigation = useNavigation(); const { theme } = useTheme(); const handlePress = () => { if (sidebar?.isActive) { sidebar.toggle(); } }; const handleGoBackPress = () => { if (canGoBack) { navigation.goBack(); } else { navigation.dispatch(DrawerActions.toggleDrawer()); } }; return ( {sidebar?.isActive ? ( <> {sidebar.isCollapsed ? ( ) : ( )} {canGoBack && ( )} ) : ( {canGoBack ? ( ) : ( )} )} ); }; const AvatarButton = () => { const userProfile = useUserProfile(); const openLoginModal = useStore((state) => state.openLoginModal); const openPDSModal = useStore((state) => state.openPdsModal); const loginAction = useStore((state) => state.login); const openLoginLink = useStore((state) => state.openLoginLink); const { theme } = useTheme(); let source: ImageSourcePropType | undefined = undefined; const windowWidth = useWindowDimensions().width; const isCompact = windowWidth <= 800; if (userProfile) { source = { uri: userProfile.avatar }; return ( ); } if (isCompact) { return ( ); } return ( ); }; const useExternalItems = (): ExternalDrawerItem[] => { const streamplaceUrl = useUrl(); const { theme } = useTheme(); const defaultStreamer = useDefaultStreamer(); if (defaultStreamer) { return []; } return [ { item: React.memo(() => ), label: ( Documentation{" "} ) as any, onPress: () => { const u = new URL(streamplaceUrl); u.pathname = "/docs"; Linking.openURL(u.toString()); }, }, ]; }; // TODO: merge in ^ function CustomDrawerContent(props) { let { theme } = useTheme(); return ( } label={() => ( Documentation{" "} )} onPress={() => { const u = new URL(window.location.href); u.pathname = "/docs"; Linking.openURL(u.toString()); }} /> ); } export default function Router() { return ( ); } export function StreamplaceDrawer() { const theme = useTheme(); const { isWeb, isElectron, isNative, isBrowser } = usePlatform(); const navigation = useNavigation(); const hydrate = useStore((state) => state.hydrate); const initPushNotifications = useStore( (state) => state.initPushNotifications, ); const registerNotificationToken = useStore( (state) => state.registerNotificationToken, ); const clearNotification = useStore((state) => state.clearNotification); const pollMySegments = useStore((state) => state.pollMySegments); const showLoginModal = useStore((state) => state.showLoginModal); const closeLoginModal = useStore((state) => state.closeLoginModal); const showPdsModal = useStore((state) => state.showPdsModal); const openPdsModal = useStore((state) => state.openPdsModal); const closePdsModal = useStore((state) => state.closePdsModal); const [livePopup, setLivePopup] = useState(false); const loginAction = useStore((state) => state.login); const openLoginLink = useStore((state) => state.openLoginLink); const siteTitle = useSiteTitle(); const defaultStreamer = useDefaultStreamer(); const sidebar = useSidebarControl(); const toast = useToast(); SystemBars.setStyle("dark"); // Top-level stuff to handle push notification registration useEffect(() => { hydrate(); initPushNotifications(); }, []); const notificationToken = useNotificationToken(); const userProfile = useUserProfile(); const hydrated = useHydrated(); // check if current user is the default streamer const isDefaultStreamer = defaultStreamer && userProfile?.did === defaultStreamer; useEffect(() => { if (notificationToken) { registerNotificationToken(); } }, [notificationToken, userProfile]); // Stuff to handle incoming push notification routing const notificationDestination = useNotificationDestination(); const linkTo = useLinkTo(); const animatedDrawerStyle = useAnimatedStyle(() => { return { width: sidebar.isActive ? sidebar.animatedWidth.value : undefined, }; }); useEffect(() => { if (notificationDestination) { linkTo(notificationDestination); clearNotification(); } }, [notificationDestination]); // Top-level stuff to handle polling for live streamers useEffect(() => { let handle: NodeJS.Timeout; handle = setInterval(() => { pollMySegments(); }, 2500); pollMySegments(); return () => clearInterval(handle); }, []); const userIsLive = useLiveUser(); useBlueskyNotifications(); let foregroundColor = theme.theme.colors.text || "#fff"; // are we in the live dashboard? const [isLiveDashboard, setIsLiveDashboard] = useState(false); useEffect(() => { if (!isLiveDashboard && userIsLive && isWeb) { toast.show( "You are streaming!", "Do you want to go to your Live Dashboard?", { actionLabel: "Go", onAction: () => { navigation.navigate("LiveDashboard"); setLivePopup(false); }, onClose: () => setLivePopup(false), variant: "error", duration: 8, }, ); } }, [userIsLive]); const externalItems = useExternalItems(); if (!hydrated) { return ; } return ( <> ( <> {/* this is a hack to give the popup the navigator context */} ), headerRight: () => , drawerActiveTintColor: "#a0287c33", unmountOnBlur: true, }} drawerContent={ sidebar.isActive ? (props) => ( { closePdsModal(); loginAction(pdsHost, openLoginLink); }} /> ); } export const PopupChecker = ({ setIsLiveDashboard, }: { setIsLiveDashboard: (isLiveDashboard: boolean) => void; }) => { const route = useRoute(); useEffect(() => { if (route.name === "LiveDashboard") { setIsLiveDashboard(true); } else { setIsLiveDashboard(false); } }, [route.name]); return ; }; const MainTab = () => { const { isWeb } = usePlatform(); const siteTitle = useSiteTitle(); const defaultStreamer = useDefaultStreamer(); return ( ( ), headerRight: () => , headerShown: !isWeb, }} > ); }; const SettingsStack = () => { const { isWeb } = usePlatform(); return ( ( ), headerRight: () => , }} > Branding Admin, drawerItemStyle: { display: "none" }, }} /> ); };