Live video on the AT Protocol
79
fork

Configure Feed

Select the types of activity you want to include in your feed.

at v0.7.19 644 lines 18 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 ArrowLeft, 20 Book, 21 Download, 22 ExternalLink, 23 Home, 24 LogIn, 25 Menu, 26 Notebook, 27 PanelLeftClose, 28 PanelLeftOpen, 29 Settings as SettingsIcon, 30 ShieldQuestion, 31 User, 32 Video, 33} from "@tamagui/lucide-icons"; 34import { useToastController } from "@tamagui/toast"; 35import { Provider, Settings } from "components"; 36import AQLink from "components/aqlink"; 37import Login from "components/login/login"; 38import Popup from "components/popup"; 39import Sidebar, { ExternalDrawerItem } from "components/sidebar/sidebar"; 40import * as ExpoLinking from "expo-linking"; 41import { hydrate, selectHydrated } from "features/base/baseSlice"; 42import { selectUserProfile } from "features/bluesky/blueskySlice"; 43import { 44 clearNotification, 45 initPushNotifications, 46 registerNotificationToken, 47 selectNotificationDestination, 48 selectNotificationToken, 49} from "features/platform/platformSlice.native"; 50import { pollMySegments } from "features/streamplace/streamplaceSlice"; 51import { useLiveUser } from "hooks/useLiveUser"; 52import usePlatform from "hooks/usePlatform"; 53import { useSidebarControl } from "hooks/useSidebarControl"; 54import { Fragment, ReactElement, useEffect, useState } from "react"; 55import { 56 ImageBackground, 57 ImageSourcePropType, 58 Linking, 59 Platform, 60 Pressable, 61 StatusBar, 62} from "react-native"; 63import { useAppDispatch, useAppSelector } from "store/hooks"; 64import { H3, Text, useTheme, View } from "tamagui"; 65import AboutScreen from "./screens/about"; 66import AppReturnScreen from "./screens/app-return"; 67import PopoutChat from "./screens/chat-popout"; 68import DownloadScreen from "./screens/download"; 69import EmbedScreen from "./screens/embed"; 70import InfoWidgetEmbed from "./screens/info-widget-embed"; 71import LiveDashboard from "./screens/live-dashboard"; 72import MultiScreen from "./screens/multi"; 73import SupportScreen from "./screens/support"; 74 75// probabl should move this 76import SignUp from "components/login/signup"; 77import KeyManager from "components/settings/key-manager"; 78import { loadStateFromStorage } from "features/base/sidebarSlice"; 79import { store } from "store/store"; 80import HomeScreen from "./screens/home"; 81 82import { useUrl } from "@streamplace/components"; 83import Constants from "expo-constants"; 84import { SystemBars } from "react-native-edge-to-edge"; 85import { 86 configureReanimatedLogger, 87 ReanimatedLogLevel, 88} from "react-native-reanimated"; 89import MobileGoLive from "./screens/mobile-go-live"; 90import MobileStream from "./screens/mobile-stream"; 91store.dispatch(loadStateFromStorage()); 92 93const Stack = createNativeStackNavigator(); 94 95// disabled strict b/c chat swipeable triggers it a LOT and the resulting logging 96// slows down the whole app 97configureReanimatedLogger({ 98 level: ReanimatedLogLevel.warn, 99 strict: false, 100}); 101 102type HomeStackParamList = { 103 StreamList: undefined; 104 Stream: { user: string }; 105}; 106 107type RootStackParamList = { 108 Home: NavigatorScreenParams<HomeStackParamList>; 109 Multi: { config: string }; 110 Support: undefined; 111 Settings: undefined; 112 KeyManagement: undefined; 113 GoLive: undefined; 114 LiveDashboard: undefined; 115 Login: undefined; 116 Signup: undefined; 117 AVSync: undefined; 118 AppReturn: { scheme: string }; 119 About: undefined; 120 Download: undefined; 121 PopoutChat: { user: string }; 122 Embed: { user: string }; 123 InfoWidgetEmbed: undefined; 124 LegacyStream: { user: string }; 125 MobileGoLive: undefined; 126}; 127 128declare global { 129 namespace ReactNavigation { 130 interface RootParamList extends RootStackParamList {} 131 } 132} 133 134const linking: LinkingOptions<ReactNavigation.RootParamList> = { 135 prefixes: [ExpoLinking.createURL("")], 136 config: { 137 screens: { 138 Home: { 139 screens: { 140 StreamList: "", 141 Stream: { 142 path: ":user", 143 }, 144 }, 145 }, 146 Multi: "multi/:config", 147 Support: "support", 148 Settings: "settings", 149 KeyManagement: "key-management", 150 GoLive: "golive", 151 LiveDashboard: "live", 152 Login: "login", 153 Signup: "signup", 154 AVSync: "sync-test", 155 AppReturn: "app-return/:scheme", 156 About: "about", 157 Download: "download", 158 PopoutChat: "chat-popout/:user", 159 Embed: "embed/:user", 160 InfoWidgetEmbed: "info-widget", 161 LegacyStream: "legacy/:user", 162 MobileGoLive: "mobile-golive", 163 }, 164 }, 165}; 166 167const associatedDomain = Constants.expoConfig?.ios?.associatedDomains?.[0]; 168if (associatedDomain && associatedDomain.startsWith("applinks:")) { 169 const domain = associatedDomain.slice("applinks:".length); 170 linking.prefixes.push(`https://${domain}`); 171} 172 173// https://github.com/streamplace/streamplace/issues/377 174const hasDevDomain = linking.prefixes.some((prefix) => 175 prefix.includes("tv.aquareum.dev"), 176); 177if (hasDevDomain) { 178 linking.prefixes.push("tv.aquareum://"); 179 linking.prefixes.push("https://stream.place"); 180} 181 182console.log("Linking prefixes", linking.prefixes); 183 184const Drawer = createDrawerNavigator(); 185 186const NavigationButton = ({ canGoBack }: { canGoBack?: boolean }) => { 187 const sidebar = useSidebarControl(); 188 const navigation = useNavigation(); 189 190 const handlePress = () => { 191 if (sidebar?.isActive) { 192 sidebar.toggle(); 193 } 194 }; 195 196 const handleGoBackPress = () => { 197 if (canGoBack) { 198 navigation.goBack(); 199 } else { 200 navigation.dispatch(DrawerActions.toggleDrawer()); 201 } 202 }; 203 204 let icon: ReactElement | null = null; 205 if (sidebar?.isActive) { 206 if (sidebar.isCollapsed) { 207 icon = <PanelLeftOpen />; 208 } else { 209 icon = <PanelLeftClose />; 210 } 211 } 212 213 return ( 214 <View 215 flexDirection="row" 216 marginLeft={Platform.OS === "android" ? "$0" : "$3"} 217 marginRight={Platform.OS === "android" ? "$3" : "$0"} 218 > 219 {icon && ( 220 <Pressable style={{ padding: 5 }} onPress={handlePress}> 221 {icon} 222 </Pressable> 223 )} 224 <Pressable style={{ padding: 5 }} onPress={handleGoBackPress}> 225 {canGoBack ? <ArrowLeft /> : sidebar?.isActive || <Menu />} 226 </Pressable> 227 </View> 228 ); 229}; 230 231const AvatarButton = () => { 232 const userProfile = useAppSelector(selectUserProfile); 233 let source: ImageSourcePropType | undefined = undefined; 234 let opacity = 1; 235 if (userProfile) { 236 source = { uri: userProfile.avatar }; 237 opacity = 0; 238 } 239 return ( 240 <AQLink to={{ screen: "Login", params: {} }}> 241 <ImageBackground 242 // defeat cursed-ass caching on ios; image sticks around when source is undefined 243 key={source?.uri ?? "default"} 244 source={source} 245 style={{ 246 width: 40, 247 height: 40, 248 borderRadius: 20, 249 overflow: "hidden", 250 marginRight: 10, 251 backgroundColor: "black", 252 justifyContent: "center", 253 alignItems: "center", 254 }} 255 > 256 <User opacity={opacity}></User> 257 </ImageBackground> 258 </AQLink> 259 ); 260}; 261 262const useExternalItems = (): ExternalDrawerItem[] => { 263 const streamplaceUrl = useUrl(); 264 return [ 265 { 266 item: Book as any, 267 label: ( 268 <Text alignSelf="flex-start"> 269 Documentation{" "} 270 <ExternalLink size={16} paddingLeft={4} position="relative" top={2} /> 271 </Text> 272 ) as any, 273 onPress: () => { 274 const u = new URL(streamplaceUrl); 275 u.pathname = "/docs"; 276 Linking.openURL(u.toString()); 277 }, 278 }, 279 ]; 280}; 281 282// TODO: merge in ^ 283function CustomDrawerContent(props) { 284 return ( 285 <DrawerContentScrollView {...props}> 286 <DrawerItemList {...props} /> 287 <DrawerItem 288 icon={() => <Book />} 289 label={() => ( 290 <Text alignSelf="flex-start"> 291 Documentation{" "} 292 <ExternalLink size={16} pl={4} position="relative" top={2} /> 293 </Text> 294 )} 295 onPress={() => { 296 const u = new URL(window.location.href); 297 u.pathname = "/docs"; 298 Linking.openURL(u.toString()); 299 }} 300 /> 301 </DrawerContentScrollView> 302 ); 303} 304 305export default function Router() { 306 return ( 307 <Provider linking={linking}> 308 <StreamplaceDrawer /> 309 </Provider> 310 ); 311} 312 313export function StreamplaceDrawer() { 314 const theme = useTheme(); 315 const { isWeb, isElectron, isNative, isBrowser } = usePlatform(); 316 const navigation = useNavigation(); 317 const dispatch = useAppDispatch(); 318 const [poppedUp, setPoppedUp] = useState(false); 319 const [livePopup, setLivePopup] = useState(false); 320 321 const sidebar = useSidebarControl(); 322 323 SystemBars.setStyle("dark"); 324 325 // Top-level stuff to handle push notification registration 326 useEffect(() => { 327 dispatch(hydrate()); 328 dispatch(initPushNotifications()); 329 }, []); 330 const notificationToken = useAppSelector(selectNotificationToken); 331 const userProfile = useAppSelector(selectUserProfile); 332 const hydrated = useAppSelector(selectHydrated); 333 useEffect(() => { 334 if (notificationToken) { 335 dispatch(registerNotificationToken()); 336 } 337 }, [notificationToken, userProfile]); 338 339 // Stuff to handle incoming push notification routing 340 const notificationDestination = useAppSelector(selectNotificationDestination); 341 const linkTo = useLinkTo(); 342 343 useEffect(() => { 344 if (notificationDestination) { 345 linkTo(notificationDestination); 346 dispatch(clearNotification()); 347 } 348 }, [notificationDestination]); 349 350 // Top-level stuff to handle polling for live streamers 351 useEffect(() => { 352 let handle: NodeJS.Timeout; 353 handle = setInterval(() => { 354 dispatch(pollMySegments()); 355 }, 2500); 356 dispatch(pollMySegments()); 357 return () => clearInterval(handle); 358 }, []); 359 360 const userIsLive = useLiveUser(); 361 const toast = useToastController(); 362 363 const [isLiveDashboard, setIsLiveDashboard] = useState(true); 364 useEffect(() => { 365 if (!isLiveDashboard && userIsLive && !poppedUp) { 366 setPoppedUp(true); 367 setLivePopup(true); 368 } 369 }, [userIsLive, poppedUp]); 370 const externalItems = useExternalItems(); 371 372 if (!hydrated) { 373 return <View />; 374 } 375 return ( 376 <> 377 <StatusBar barStyle="light-content" /> 378 <Drawer.Navigator 379 initialRouteName="Home" 380 screenOptions={{ 381 // for the custom sidebar 382 drawerType: sidebar.isActive ? "permanent" : "front", 383 swipeEnabled: !sidebar.isActive, 384 drawerStyle: { 385 // afaict the drawer is a RN Animated component internally 386 width: sidebar.isActive 387 ? (sidebar.animatedWidth as any) 388 : undefined, 389 }, 390 // rest 391 headerLeft: () => ( 392 <> 393 {/* this is a hack to give the popup the navigator context */} 394 <PopupChecker setIsLiveDashboard={setIsLiveDashboard} /> 395 <NavigationButton /> 396 </> 397 ), 398 headerRight: () => <AvatarButton />, 399 drawerActiveTintColor: theme.accentColor.val, 400 unmountOnBlur: true, 401 }} 402 drawerContent={ 403 sidebar.isActive 404 ? (props) => ( 405 <Sidebar 406 {...props} 407 collapsed={sidebar.isCollapsed} 408 hidden={sidebar.isHidden} 409 widthAnim={sidebar.animatedWidth} 410 externalItems={externalItems} 411 /> 412 ) 413 : CustomDrawerContent 414 } 415 > 416 <Drawer.Screen 417 name="Home" 418 component={MainTab} 419 options={{ 420 drawerIcon: () => <Home />, 421 drawerLabel: () => <Text>Home</Text>, 422 headerTitle: "Streamplace", 423 headerShown: isWeb, 424 title: "Streamplace", 425 }} 426 listeners={{ 427 drawerItemPress: (e) => { 428 e.preventDefault(); 429 navigation.dispatch( 430 CommonActions.reset({ 431 index: 0, 432 routes: [ 433 { 434 name: "Home", 435 state: { 436 routes: [{ name: "StreamList" }], 437 }, 438 }, 439 ], 440 }), 441 ); 442 }, 443 }} 444 /> 445 <Drawer.Screen 446 name="About" 447 component={AboutScreen} 448 options={{ 449 drawerLabel: () => <Text>What's Streamplace?</Text>, 450 drawerIcon: () => <ShieldQuestion />, 451 drawerItemStyle: isNative ? { display: "none" } : undefined, 452 }} 453 /> 454 <Drawer.Screen 455 name="Download" 456 component={DownloadScreen} 457 options={{ 458 drawerLabel: () => <Text>Download</Text>, 459 drawerIcon: () => <Download />, 460 drawerItemStyle: isBrowser ? undefined : { display: "none" }, 461 }} 462 /> 463 <Drawer.Screen 464 name="Settings" 465 component={Settings} 466 options={{ 467 drawerIcon: () => <SettingsIcon />, 468 drawerLabel: () => <Text>Settings</Text>, 469 }} 470 /> 471 472 <Drawer.Screen 473 name="KeyManagement" 474 component={KeyManager} 475 options={{ 476 drawerLabel: () => <Text>Key Manager</Text>, 477 drawerItemStyle: { display: "none" }, 478 }} 479 /> 480 <Drawer.Screen 481 name="Support" 482 component={SupportScreen} 483 options={{ 484 drawerLabel: () => <Text>Support</Text>, 485 drawerItemStyle: { display: "none" }, 486 }} 487 /> 488 <Drawer.Screen 489 name="LiveDashboard" 490 component={LiveDashboard} 491 options={{ 492 drawerLabel: () => <Text>Live Dashboard</Text>, 493 drawerIcon: () => <Video />, 494 drawerItemStyle: isNative ? { display: "none" } : undefined, 495 }} 496 /> 497 <Drawer.Screen 498 name="AppReturn" 499 component={AppReturnScreen} 500 options={{ 501 drawerLabel: () => null, 502 drawerItemStyle: { display: "none" }, 503 headerShown: false, 504 }} 505 /> 506 <Drawer.Screen 507 name="Multi" 508 component={MultiScreen} 509 options={{ 510 drawerLabel: () => null, 511 drawerItemStyle: { display: "none" }, 512 }} 513 /> 514 <Drawer.Screen 515 name="Login" 516 component={Login} 517 options={{ 518 drawerIcon: () => <LogIn />, 519 drawerLabel: () => <Text>Login</Text>, 520 }} 521 /> 522 <Drawer.Screen 523 name="Signup" 524 component={SignUp} 525 options={{ 526 drawerIcon: () => <Notebook />, 527 drawerItemStyle: { display: "none" }, 528 drawerLabel: () => <Text>Sign up</Text>, 529 }} 530 /> 531 <Drawer.Screen 532 name="PopoutChat" 533 component={PopoutChat} 534 options={{ 535 drawerLabel: () => null, 536 drawerItemStyle: { display: "none" }, 537 headerShown: false, 538 drawerStyle: { display: "none" }, 539 }} 540 /> 541 <Drawer.Screen 542 name="Embed" 543 component={EmbedScreen} 544 options={{ 545 drawerLabel: () => null, 546 drawerItemStyle: { display: "none" }, 547 headerShown: false, 548 }} 549 /> 550 <Drawer.Screen 551 name="InfoWidgetEmbed" 552 component={InfoWidgetEmbed} 553 options={{ 554 drawerLabel: () => null, 555 drawerItemStyle: { display: "none" }, 556 headerShown: false, 557 }} 558 /> 559 <Drawer.Screen 560 name="MobileGoLive" 561 component={MobileGoLive} 562 options={{ 563 headerTitle: "Go Live", 564 drawerItemStyle: isNative ? undefined : { display: "none" }, 565 drawerLabel: () => <Text>Go Live</Text>, 566 title: "Go live", 567 drawerIcon: () => <Video />, 568 headerShown: false, 569 }} 570 /> 571 </Drawer.Navigator> 572 {isWeb && livePopup && ( 573 <Popup 574 onPress={() => { 575 navigation.navigate("LiveDashboard"); 576 setLivePopup(false); 577 }} 578 onClose={() => { 579 setLivePopup(false); 580 }} 581 containerProps={{ 582 bottom: "$8", 583 }} 584 bubbleProps={{ 585 cursor: "pointer", 586 backgroundColor: "#cc0000", 587 }} 588 > 589 <H3 textAlign="center">YOU ARE LIVE!!!</H3> 590 <Text> 591 {isNative ? "Tap" : "Click"} here to go to the live dashboard 592 </Text> 593 </Popup> 594 )} 595 </> 596 ); 597} 598 599export const PopupChecker = ({ 600 setIsLiveDashboard, 601}: { 602 setIsLiveDashboard: (isLiveDashboard: boolean) => void; 603}) => { 604 const route = useRoute(); 605 useEffect(() => { 606 if (route.name === "LiveDashboard") { 607 setIsLiveDashboard(true); 608 } else { 609 setIsLiveDashboard(false); 610 } 611 }, [route.name]); 612 return <Fragment />; 613}; 614 615const MainTab = () => { 616 const theme = useTheme(); 617 const { isWeb } = usePlatform(); 618 return ( 619 <Stack.Navigator 620 initialRouteName="StreamList" 621 screenOptions={{ 622 headerLeft: ({ canGoBack }) => ( 623 <NavigationButton canGoBack={canGoBack} /> 624 ), 625 headerRight: () => <AvatarButton />, 626 headerShown: !isWeb, 627 }} 628 > 629 <Stack.Screen 630 name="StreamList" 631 component={HomeScreen} 632 options={{ headerTitle: "Streamplace", title: "Streamplace" }} 633 /> 634 <Stack.Screen 635 name="Stream" 636 component={MobileStream} 637 options={{ 638 headerTitle: "Stream", 639 title: "Streamplace Stream", 640 }} 641 /> 642 </Stack.Navigator> 643 ); 644};