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