Live video on the AT Protocol
79
fork

Configure Feed

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

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