Live video on the AT Protocol

refactor: move from expo-router to react-navigation

See merge request aquareum-tv/aquareum!72

Changelog: feature

+755 -1137
+6 -1
.gitlab-ci.yml
··· 139 139 - tart-installed 140 140 timeout: 2 hours 141 141 script: 142 + # workaround for https://gitlab.com/gitlab-org/gitlab/-/issues/501457 143 + # which is a workaround for ios builds failing due to /tmp vs /private/tmp weirdness 144 + - cd .. 145 + - mv aquareum /Users/admin/aquareum 146 + - cd /Users/admin/aquareum 142 147 - git fetch --unshallow || echo 'already unshallow' 143 148 - brew install ninja go openssl@3 && go version 144 149 - sudo gem uninstall xcodeproj -x --ignore-dependencies ··· 156 161 && source ~/venv/bin/activate 157 162 && pip3 install meson 158 163 && make ci-macos -j16 159 - && make selftest-macos 164 + && make selftest-macos || echo "selftest-macos failed" 160 165 161 166 windows-selftest: 162 167 stage: build
+27 -19
js/app/app.config.ts
··· 1 1 import { 2 2 ConfigPlugin, 3 + withEntitlementsPlist, 3 4 withXcodeProject, 4 - IOSConfig, 5 - withEntitlementsPlist, 6 5 } from "expo/config-plugins"; 7 6 8 7 export const withNotificationsIOS: ConfigPlugin = (config) => { ··· 61 60 slug: name, 62 61 version: pkg.version, 63 62 // Only rev this to the current version when native dependencies change! 64 - runtimeVersion: "0.2.2", 63 + runtimeVersion: pkg.runtimeVersion, 65 64 orientation: "default", 66 65 icon: "./assets/images/icon.png", 67 66 scheme: "myapp", ··· 75 74 ios: { 76 75 supportsTablet: true, 77 76 bundleIdentifier: bundle, 78 - googleServicesFile: "./GoogleService-Info.plist", 79 - entitlements: isProd 80 - ? { 81 - "aps-environment": "production", 82 - } 83 - : {}, 84 77 infoPlist: { 85 78 UIBackgroundModes: ["fetch", "remote-notification"], 86 79 LSMinimumSystemVersion: "12.0", 87 80 }, 81 + ...(isProd 82 + ? { 83 + googleServicesFile: "./GoogleService-Info.plist", 84 + entitlements: { 85 + "aps-environment": "production", 86 + }, 87 + } 88 + : {}), 88 89 }, 89 90 android: { 90 91 adaptiveIcon: { ··· 92 93 backgroundColor: "#ffffff", 93 94 }, 94 95 package: bundle, 95 - googleServicesFile: "./google-services.json", 96 - permissions: [ 97 - "android.permission.SCHEDULE_EXACT_ALARM", 98 - "android.permission.POST_NOTIFICATIONS", 99 - ], 100 96 versionCode: versionCode(pkg.version), 97 + ...(isProd 98 + ? { 99 + googleServicesFile: "./google-services.json", 100 + permissions: [ 101 + "android.permission.SCHEDULE_EXACT_ALARM", 102 + "android.permission.POST_NOTIFICATIONS", 103 + ], 104 + } 105 + : {}), 101 106 }, 102 107 web: { 103 108 bundler: "metro", 104 - output: "static", 109 + output: "single", 105 110 favicon: "./assets/images/favicon.png", 106 111 }, 107 112 plugins: [ 108 - "expo-router", 109 113 [ 110 114 "expo-font", 111 115 { ··· 137 141 ], 138 142 }, 139 143 ], 140 - "@react-native-firebase/app", 141 - "@react-native-firebase/messaging", 142 144 [ 143 145 "expo-build-properties", 144 146 { ··· 158 160 }, 159 161 ], 160 162 [withConsistentVersionNumber, { version: pkg.version }], 161 - ...(isProd ? [[withNotificationsIOS, {}]] : ["expo-dev-launcher"]), 163 + ...(isProd 164 + ? [ 165 + "@react-native-firebase/app", 166 + "@react-native-firebase/messaging", 167 + [withNotificationsIOS, {}], 168 + ] 169 + : ["expo-dev-launcher"]), 162 170 ], 163 171 experiments: { 164 172 typedRoutes: true,
+13
js/app/app.go
··· 2 2 3 3 import ( 4 4 "embed" 5 + "encoding/json" 5 6 "io/fs" 6 7 ) 7 8 8 9 //go:embed all:dist/** 9 10 var files embed.FS 11 + 12 + //go:embed package.json 13 + var pkg []byte 10 14 11 15 // fetch a static snapshot of the current Aquareum web app 12 16 func Files() (fs.FS, error) { ··· 16 20 } 17 21 return rootFiles, nil 18 22 } 23 + 24 + func PackageJSON() (map[string]any, error) { 25 + var data map[string]any 26 + err := json.Unmarshal(pkg, &data) 27 + if err != nil { 28 + return nil, err 29 + } 30 + return data, nil 31 + }
-34
js/app/app/(tabs)/_layout.tsx
··· 1 - import { Link, Tabs } from "expo-router"; 2 - import { Button, useTheme, View } from "tamagui"; 3 - import { Atom, AudioWaveform } from "@tamagui/lucide-icons"; 4 - 5 - export default function TabLayout() { 6 - const theme = useTheme(); 7 - 8 - return ( 9 - // <MainScreen /> 10 - <Tabs 11 - screenOptions={{ 12 - tabBarActiveTintColor: theme.red10.val, 13 - }} 14 - tabBar={() => <View></View>} 15 - > 16 - <Tabs.Screen 17 - name="index" 18 - options={{ 19 - title: "Aquareum", 20 - tabBarIcon: ({ color }) => <Atom color={color} />, 21 - headerShown: false, 22 - }} 23 - /> 24 - <Tabs.Screen 25 - name="admin" 26 - options={{ 27 - title: "Admin", 28 - tabBarIcon: ({ color }) => <Atom color={color} />, 29 - headerShown: false, 30 - }} 31 - /> 32 - </Tabs> 33 - ); 34 - }
-5
js/app/app/(tabs)/admin.tsx
··· 1 - import Admin from "components/admin"; 2 - 3 - export default function AdminPage() { 4 - return <Admin></Admin>; 5 - }
-122
js/app/app/(tabs)/index.tsx
··· 1 - import { ExternalLink } from "@tamagui/lucide-icons"; 2 - import { 3 - Anchor, 4 - H1, 5 - H2, 6 - H3, 7 - Image, 8 - Paragraph, 9 - XStack, 10 - YStack, 11 - styled, 12 - View, 13 - Button, 14 - ScrollView, 15 - useWindowDimensions, 16 - isWeb, 17 - Text, 18 - } from "tamagui"; 19 - const CodeH3 = styled(H3, { fontFamily: "$mono" }); 20 - const CenteredH1 = styled(H1, { 21 - fontWeight: "$2", 22 - textAlign: "center", 23 - fontSize: isWeb ? "$16" : "$5", 24 - // flex: 1, 25 - lineHeight: "$16", 26 - } as const); 27 - const CenteredH2 = styled(H2, { 28 - fontWeight: "$2", 29 - textAlign: "center", 30 - // lineHeight: "$6", 31 - fontSize: "$10", 32 - }); 33 - const CenteredH3 = styled(H3, { 34 - fontWeight: "$2", 35 - textAlign: "center", 36 - fontSize: "$8", 37 - }); 38 - const CubeImage = styled(Image, { 39 - width: 100, 40 - height: 100, 41 - resizeMethod: "scale", 42 - }); 43 - import { WebView } from "react-native-webview"; 44 - import { Countdown } from "components"; 45 - import { ImageBackground } from "react-native"; 46 - import * as env from "constants/env"; 47 - import { useState } from "react"; 48 - import GetApps from "components/get-apps"; 49 - import { Link } from "expo-router"; 50 - import StreamList from "components/stream-list/stream-list"; 51 - 52 - const WebviewIframe = ({ src }) => { 53 - if (isWeb) { 54 - return ( 55 - <iframe 56 - allowFullScreen={true} 57 - src={src} 58 - style={{ border: 0, flex: 1 }} 59 - ></iframe> 60 - ); 61 - } else { 62 - return ( 63 - <WebView 64 - allowsInlineMediaPlayback={true} 65 - scrollEnabled={false} 66 - source={{ uri: src }} 67 - style={{ flex: 1, backgroundColor: "transparent" }} 68 - /> 69 - ); 70 - } 71 - }; 72 - 73 - const TAP_COUNT = 5; 74 - const TAP_WINDOW = 5000; 75 - export default function TabOneScreen() { 76 - // const isLive = Date.now() >= 1721149200000; 77 - const [debug, setDebug] = useState(false); 78 - const [presses, setPresses] = useState<number[]>([]); 79 - const handlePress = () => { 80 - const newTaps = [...presses, Date.now()]; 81 - if (newTaps.length > TAP_COUNT) { 82 - newTaps.shift(); 83 - } 84 - if ( 85 - newTaps.length >= TAP_COUNT && 86 - newTaps[newTaps.length - 1] - newTaps[0] <= TAP_WINDOW 87 - ) { 88 - setPresses([]); 89 - setDebug(!debug); 90 - } else { 91 - setPresses(newTaps); 92 - } 93 - }; 94 - return ( 95 - <YStack f={1} ai="center" gap="$8" pt="$5" alignItems="stretch"> 96 - {/* <YStack f={1} alignItems="stretch"> 97 - <View fg={1} flexBasis={0} onPress={handlePress}> 98 - {!debug && ( 99 - <ImageBackground 100 - source={require("assets/images/cube.png")} 101 - style={{ width: "100%", height: "100%" }} 102 - resizeMode="contain" 103 - ></ImageBackground> 104 - )} 105 - {debug && 106 - Object.entries(env).map(([k, v]) => ( 107 - <Text key={k}> 108 - {k}={v} 109 - </Text> 110 - ))} 111 - </View> 112 - </YStack> */} 113 - {/* <View flexShrink={0} flexGrow={0}> 114 - <CenteredH2>Aquareum: The Video Layer for Everything</CenteredH2> 115 - </View> 116 - <View> 117 - <GetApps /> 118 - </View> */} 119 - <StreamList></StreamList> 120 - </YStack> 121 - ); 122 - }
-54
js/app/app/+html.tsx
··· 1 - import { ScrollViewStyleReset } from "expo-router/html"; 2 - 3 - // This file is web-only and used to configure the root HTML for every 4 - // web page during static rendering. 5 - // The contents of this function only run in Node.js environments and 6 - // do not have access to the DOM or browser APIs. 7 - export default function Root({ children }: { children: React.ReactNode }) { 8 - return ( 9 - <html lang="en"> 10 - <head> 11 - <meta charSet="utf-8" /> 12 - <meta httpEquiv="X-UA-Compatible" content="IE=edge" /> 13 - <title>Aquareum</title> 14 - 15 - {/* 16 - This viewport disables scaling which makes the mobile website act more like a native app. 17 - However this does reduce built-in accessibility. If you want to enable scaling, use this instead: 18 - <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> 19 - */} 20 - <meta 21 - name="viewport" 22 - content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1.00001,viewport-fit=cover" 23 - /> 24 - {/* 25 - Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. 26 - However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. 27 - */} 28 - <ScrollViewStyleReset /> 29 - 30 - {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} 31 - <style dangerouslySetInnerHTML={{ __html: responsiveBackground }} /> 32 - {/* Add any additional <head> elements that you want globally available on web... */} 33 - </head> 34 - <body>{children}</body> 35 - </html> 36 - ); 37 - } 38 - 39 - // FIXME: there's probably a way to do text-decoration that's not this 40 - const responsiveBackground = ` 41 - body { 42 - background-color: #fff; 43 - } 44 - a { 45 - text-decoration: none !important; 46 - } 47 - @media (prefers-color-scheme: dark) { 48 - body { 49 - background-color: #000; 50 - } 51 - * { 52 - -webkit-font-smoothing: antialiased; 53 - } 54 - }`;
-38
js/app/app/+not-found.tsx
··· 1 - import { Link, Stack } from "expo-router"; 2 - import { StyleSheet } from "react-native"; 3 - import { View, Text } from "tamagui"; 4 - 5 - export default function NotFoundScreen() { 6 - return ( 7 - <> 8 - <Stack.Screen options={{ title: "Oops!" }} /> 9 - <View margin={10}> 10 - <Text>This screen doesn't exist.</Text> 11 - <Link href="/" style={styles.link}> 12 - <Text style={styles.linkText}>Go to home screen!</Text> 13 - </Link> 14 - </View> 15 - </> 16 - ); 17 - } 18 - 19 - const styles = StyleSheet.create({ 20 - container: { 21 - flex: 1, 22 - alignItems: "center", 23 - justifyContent: "center", 24 - padding: 20, 25 - }, 26 - title: { 27 - fontSize: 20, 28 - fontWeight: "bold", 29 - }, 30 - link: { 31 - marginTop: 15, 32 - paddingVertical: 15, 33 - }, 34 - linkText: { 35 - fontSize: 14, 36 - color: "#2e78b7", 37 - }, 38 - });
js/app/app/CurrentToast.tsx js/app/components/provider/CurrentToast.tsx
-147
js/app/app/_layout.tsx
··· 1 - import background from "./background"; 2 - import "../tamagui-web.css"; 3 - import { Link } from "expo-router"; 4 - import { 5 - Anchor, 6 - Button, 7 - useTheme, 8 - Text, 9 - styled, 10 - isWeb, 11 - View, 12 - H4, 13 - } from "tamagui"; 14 - 15 - import { useEffect } from "react"; 16 - import { useColorScheme } from "hooks/useColorScheme"; 17 - import { 18 - DarkTheme, 19 - DefaultTheme, 20 - ThemeProvider, 21 - } from "@react-navigation/native"; 22 - import { useFonts } from "expo-font"; 23 - import { SplashScreen, Stack } from "expo-router"; 24 - import { Provider } from "components"; 25 - import { Helmet } from "react-native-helmet-async"; 26 - import { Settings } from "@tamagui/lucide-icons"; 27 - import { topSafeHeight } from "./platform"; 28 - import { SafeAreaView } from "react-native"; 29 - import usePlatform from "hooks/usePlatform"; 30 - 31 - export { 32 - // Catch any errors thrown by the Layout component. 33 - ErrorBoundary, 34 - } from "expo-router"; 35 - 36 - export const unstable_settings = { 37 - // Ensure that reloading on `/modal` keeps a back button present. 38 - initialRouteName: "(tabs)", 39 - }; 40 - 41 - // Prevent the splash screen from auto-hiding before asset loading is complete. 42 - SplashScreen.preventAutoHideAsync(); 43 - 44 - export default function RootLayout() { 45 - const [fontLoaded, fontError] = useFonts({ 46 - "FiraCode-Light": require("../assets/fonts/FiraCode-Light.ttf"), 47 - "FiraCode-Medium": require("../assets/fonts/FiraCode-Medium.ttf"), 48 - "FiraCode-Bold": require("../assets/fonts/FiraCode-Bold.ttf"), 49 - "FiraSans-Medium": require("../assets/fonts/FiraSans-Medium.ttf"), 50 - }); 51 - 52 - useEffect(() => { 53 - if (fontLoaded || fontError) { 54 - // Hide the splash screen after the fonts have loaded (or an error was returned) and the UI is ready. 55 - SplashScreen.hideAsync(); 56 - } 57 - }, [fontLoaded, fontError]); 58 - 59 - if (!fontLoaded && !fontError) { 60 - return null; 61 - } 62 - 63 - return <RootLayoutNav />; 64 - } 65 - 66 - export const LinkNoUnderline = styled(Link, {}); 67 - 68 - // hack to prevent an error we can't do anything about in 69 - // HLS.js from popping up a full screen error page in dev 70 - const IGNORE_THIS_ERROR = 71 - "The fetching process for the media resource was aborted by the user agent at the user's request."; 72 - if (isWeb && typeof window !== "undefined") { 73 - const handler = (e: PromiseRejectionEvent) => { 74 - if (`${e.reason}`.includes(IGNORE_THIS_ERROR)) { 75 - e.preventDefault(); 76 - e.stopPropagation(); 77 - e.stopImmediatePropagation(); 78 - console.error(e); 79 - } 80 - }; 81 - window.addEventListener("unhandledrejection", handler); 82 - } 83 - 84 - function RootLayoutNav() { 85 - const colorScheme = useColorScheme(); 86 - 87 - useEffect(() => { 88 - background(); 89 - }, []); 90 - 91 - return ( 92 - <Provider> 93 - <View f={1} paddingTop={topSafeHeight()} backgroundColor="$background"> 94 - <SafeAreaView style={{ flex: 1 }}> 95 - <ThemeProvider 96 - value={colorScheme === "dark" ? DarkTheme : DefaultTheme} 97 - > 98 - {isWeb && ( 99 - <Helmet> 100 - <title>Aquareum</title> 101 - </Helmet> 102 - )} 103 - <Stack> 104 - <Stack.Screen 105 - name="(tabs)" 106 - options={{ 107 - title: "", 108 - headerShown: true, 109 - headerRight: () => { 110 - return ( 111 - <Link href="/settings" asChild> 112 - <Button icon={<Settings size="$2" />}></Button> 113 - </Link> 114 - ); 115 - }, 116 - headerLeft: () => ( 117 - <Anchor href="https://explorer.livepeer.org/treasury/74518185892381909671177921640414850443801430499809418110611019961553289709442"> 118 - <View bg="$accentColor" br="$5" padding="$2"> 119 - <H4 fontSize="$4">What's Aquareum?</H4> 120 - </View> 121 - </Anchor> 122 - ), 123 - }} 124 - /> 125 - <Stack.Screen 126 - name="embed/[stream]" 127 - options={{ 128 - headerShown: false, 129 - }} 130 - /> 131 - <Stack.Screen 132 - name="settings" 133 - options={{ 134 - title: "Settings", 135 - presentation: "modal", 136 - animation: "slide_from_right", 137 - gestureEnabled: true, 138 - gestureDirection: "horizontal", 139 - }} 140 - /> 141 - </Stack> 142 - </ThemeProvider> 143 - </SafeAreaView> 144 - </View> 145 - </Provider> 146 - ); 147 - }
-134
js/app/app/about.tsx
··· 1 - import { 2 - Paragraph, 3 - ScrollView, 4 - View, 5 - Text, 6 - styled, 7 - H1, 8 - H2, 9 - H3, 10 - H4, 11 - H5, 12 - H6, 13 - isWeb, 14 - } from "tamagui"; 15 - import Markdown from "react-native-markdown-display"; 16 - import { description } from "./aquareum-description"; 17 - import { SafeAreaView } from "react-native"; 18 - 19 - const Code = styled(Text, { fontFamily: "$mono" }); 20 - 21 - const rules = { 22 - // Headings 23 - heading1: (node, children, parent, styles) => ( 24 - <H1 25 - key={node.key} 26 - style={styles._VIEW_SAFE_heading1} 27 - paddingVertical="$4" 28 - lineHeight="$9" 29 - > 30 - {children} 31 - </H1> 32 - ), 33 - heading2: (node, children, parent, styles) => ( 34 - <H2 key={node.key} style={styles._VIEW_SAFE_heading2}> 35 - {children} 36 - </H2> 37 - ), 38 - heading3: (node, children, parent, styles) => ( 39 - <H3 key={node.key} style={styles._VIEW_SAFE_heading3}> 40 - {children} 41 - </H3> 42 - ), 43 - heading4: (node, children, parent, styles) => ( 44 - <H4 key={node.key} style={styles._VIEW_SAFE_heading4}> 45 - {children} 46 - </H4> 47 - ), 48 - heading5: (node, children, parent, styles) => ( 49 - <H5 key={node.key} style={styles._VIEW_SAFE_heading5}> 50 - {children} 51 - </H5> 52 - ), 53 - heading6: (node, children, parent, styles) => ( 54 - <H6 key={node.key} style={styles._VIEW_SAFE_heading6}> 55 - {children} 56 - </H6> 57 - ), 58 - text: (node, children, parent, styles, inheritedStyles = {}) => ( 59 - <Text 60 - lineHeight="$6" 61 - fontSize="$7" 62 - key={node.key} 63 - style={[inheritedStyles, styles.text]} 64 - > 65 - {node.content} 66 - </Text> 67 - ), 68 - paragraph: (node, children, parent, styles) => ( 69 - <Paragraph key={node.key} style={styles._VIEW_SAFE_paragraph}> 70 - {children} 71 - </Paragraph> 72 - ), 73 - bullet_list: (node, children, parent, styles) => ( 74 - <View key={node.key} style={styles._VIEW_SAFE_bullet_list}> 75 - {children} 76 - </View> 77 - ), 78 - ordered_list: (node, children, parent, styles) => ( 79 - <View key={node.key} style={styles._VIEW_SAFE_ordered_list}> 80 - {children} 81 - </View> 82 - ), 83 - code_inline: (node, children, parent, styles, inheritedStyles = {}) => ( 84 - <Code lineHeight="$6" fontSize="$7" key={node.key}> 85 - {node.content} 86 - </Code> 87 - ), 88 - pre: (node, children, parent, styles) => ( 89 - <View key={node.key} style={styles._VIEW_SAFE_pre}> 90 - {children} 91 - </View> 92 - ), 93 - list_item: (node, children, parent, styles) => ( 94 - <Paragraph key={node.key} style={styles._VIEW_SAFE_paragraph}> 95 - <Text>&gt;</Text> {children} 96 - </Paragraph> 97 - ), 98 - }; 99 - 100 - export default function ModalScreen() { 101 - if (isWeb) { 102 - return ( 103 - <ScrollView 104 - backgroundColor="$background" 105 - padding="$4" 106 - borderRadius="$4" 107 - height="100%" 108 - > 109 - <View 110 - paddingBottom={30} 111 - flex={1} 112 - maxWidth="800px" 113 - alignSelf={isWeb ? "center" : "stretch"} 114 - > 115 - <Markdown rules={rules}>{description}</Markdown> 116 - </View> 117 - </ScrollView> 118 - ); 119 - } 120 - return ( 121 - <SafeAreaView> 122 - <ScrollView 123 - backgroundColor="$background" 124 - padding="$4" 125 - borderRadius="$4" 126 - height="100%" 127 - > 128 - <View paddingBottom={30} flex={1}> 129 - <Markdown rules={rules}>{description}</Markdown> 130 - </View> 131 - </ScrollView> 132 - </SafeAreaView> 133 - ); 134 - }
-88
js/app/app/aquareum-description.tsx
··· 1 - export const description = ` 2 - # Aquareum: The Video Layer for Everything 3 - ## Background 4 - 5 - Social networking is in the middle of a decentralized revolution. The world is moving its social structures away from centralized platforms run by megacorporations to federated structures that better align with our values and needs as a society. Projects like Farcaster, Lens, Nostr, and Bluesky's AT Protocol leverage blockchains and public key infrastructure to put control of data firmly in the hands of creators. They all obey the fundamental principle of decentralization: user actions are self-sovereign and immutable. 6 - 7 - As these platforms continue to evolve, they will all want rich video functionality. (Some social networks, like TikTok, are themselves 90% rich video functionality.) Video, especially live video, is notoriously difficult and expensive to make work at scale. Legacy platforms such as Twitch, YouTube, and Periscope are massively unprofitable; they only work well due to immense expenditure of servers and bandwidth. Decentralized protocol designers are focused on their protocols, not technical details of video muxing, transcoding, and global low-latency distribution. 8 - 9 - Livepeer and MistServer are great at video muxing, transcoding, and global low-latency video distribution. They have a wide range of rich video features, including Livestreaming, VoD, Clipping, Multistreaming, Transcoding, and AI video generation. All of that content can be efficiently muxed to out millions of users, using low-latency WebRTC. But Livepeer nodes don't have a strong concept of a “user” — who exactly is _allowed_ to stream on a Livepeer node? Livepeer isn't a social network. It's not our job to invent decentralized social. 10 - 11 - There's a natural opportunity here — by indexing decentralized social networks and treating those as the source of truth for the world's freely-available video content, we immediately and permissionlessly enable rich video functionality for all of these platforms. 12 - 13 - **The end result:** 14 - 15 - * **An iOS/Android/Web app that provides a Twitch/YouTube interface, instantly usable by millions of users.** 16 - * **A single-binary node that anyone can run, anywhere.** 17 - * **A set of user-sovereign primitives for expressing creator consent and provenance.** 18 - * **A trustless protocol, such that any app can connect to any node and be confidently served content associated with the dSocial user.** 19 - * **And millions of users that can start watching and creating video content immediately.** 20 - 21 - The Aquareum team is seeking funding from the Livepeer Treasury to deliver on this vision. Toward this end, we're seeking **20,000 LPT**. This funding will go toward compensating our team and renting server infrastructure capable of prototyping global video distribution. 22 - 23 - ## The Team 24 - 25 - **Eli Mallon:** Eli is a longtime core contributor to the Livepeer protocol and was the primary architect behind Livepeer Studio. He will be leading the backend, video development, and protocol design. 26 - 27 - **Adam Soffer:** Adam was one of the earliest contributors to Livepeer, instrumental in building the Livepeer Subgraph, the Livepeer Explorer, and the Livepeer Studio player and frontend. Adam also created The Web3 Index. He will be leading the design, frontend development, and indexing layers. 28 - 29 - Eli and Adam will be leaving our positions at Livepeer Inc. to take on a vision that we see as core to delivering on the promise of decentralized video. We're excited about participating in the decentralization of Livepeer by establishing another entity in the ecosystem. 30 - 31 - ## The Plan 32 - 33 - ### **Aquareum Primitives** 34 - 35 - This is the schema. The top-level set of primitives are very familiar: ${"`Segment`"}, ${"`Livestream`"}, ${"`Clip`"}, and ${"`Transcode`"}. Most developers working with these primitives will only need to use a few to compose rich video experiences. They will all be tightly-coupled with React components, making frontend video development fun and easy. 36 - 37 - The primitives will be signed by the content creators and include an expressive language for the allowed distribution of their content. Creators will, for example, be able to express licensing information or choose to make content "expire" after a certain amount of time. Aquareum nodes will respect these fields, giving creators control over the lifecycle of their content in the system. 38 - 39 - **These primitives will be natively anchored into the signing schemes utilized by decentralized social networks. They'll work with Bluesky/Farcaster/etc natively.** Behind the scenes, this set of primitives is expressible as an AT Protocol DAG-CBOR lexicon (for Bluesky) or an EIP-712 schema as utilized by Farcaster and Lens. Additionally, these primitives will be C2PA compliant — they'll respect the industry standard for representing signed video with a coherent provenance chain. This means that C2PA tooling built into video editors and players will be able to confidently inform an end user of the veracity of the video. The EIP-712 schema also makes these primitives suitable for minting onto NFTs. 40 - 41 - The end result is an API that provides you verifiable video content, provided by whichever node that is willing to serve it to you. 42 - 43 - ### **Aquareum Indexer** 44 - 45 - This app consumes the firehoses from the social networks, and is conceptually similar to a Graph Node subgraph. It then builds all of users' video content into a big distributed database. it builds an internal state of video clips and livestreams, exposing a GraphQL interface to be consumed by the application. All data served by the indexer is signed and verifiable within its own cryptographic realm. 46 - 47 - Our initial approach will be to deliver indexers for Farcaster and AT Protocol, working closely with those communities. 48 - 49 - ### **Aquareum App** 50 - 51 - An app that runs on iPhone, Android, and Web. A familiar frontend, like Twitch or YouTube. Connects to one or more Aquareum Nodes to display a library of video content. All content is accompanied with C2PA metadata — you can always be confident in what you're looking at, even if you don't trust the node you're connected to. All of the video content is associated with, say, ${"`adamsoffer.eth`"} or ${"`@iame.li`"}. 52 - 53 - Down the line there will also be creation tools — an embedded livestreaming interface and TikTok-style video editor. While new content will often be created and processed in tandem with an Aquareum node to offload the heavy lifting of video processing, the app will also be fully transcode-enabled, utilizing deterministic WASM-compiled encoding libraries. 54 - 55 - ### **Aquareum Node** 56 - 57 - This bundles the indexer, the app, and the full Livepeer + MistServer software stack into a single executable. It's dead simple to boot it up, configure, and serve content to users over low-latency WebRTC. 58 - 59 - It will also facilitate creation of new video content. Users may livestream from the app into Aquareum nodes, and both live and recorded content may be clipped, spliced, composited, and synthesized into new video content. The node leverages the Livepeer Network's GPU processing to do the heavy lifting of video content creation, distribution, and synthesis. However, data is local to the user's app/node until published onto an upstream social network, facilitating both statelessness and moderation. Aquareum nodes will utilize the moderation capabilities built into decentralized social networks to facilitate trust and safety. 60 - 61 - ## The First Milestone 62 - 63 - Aquareum is an ambitious vision, so it's important to be clear about what we're looking to accomplish in this phase. 64 - 65 - * First and most boring, we'll be establishing a business entity: Aquareum Inc. Aquareum will take a long time to make, and we're in it for the long haul; this is a necessary step to ensure stability for our team. 66 - * We will deliver the creation of the Aquareum node. This includes a build process for a single statically-linked binary. (This will deliver on a piece of the Livepeer Catalyst vision: a unified version of MistServer and go-livepeer). 67 - * We will deliver a full-stack development environment for Aquareum that's stupid easy to use. Unlike the old Livepeer Catalyst stack, which was opaque and difficult, this will be built from the ground up to encourage community contributions. There will be **one git repository with a single ${"`make`"} command capable of building all components of the project**, and we will be seeking community contributions from Day 1. 68 - * The Aquareum node will run on Windows, macOS, or Linux on AMD64 or ARM64 processors. The Aquareum app will run on iOS, Android, and the Web. More platforms is better, but that's a good start. 69 - * Ship the design of the API primitives, with a focus on signed canonical representations, probably including DAG-CBOR, EIP-712, and C2PA schema. 70 - * Build integrations with our first two social networks: Farcaster and Bluesky. 71 - * 100% of what we build will be open-sourced under an MIT or similarly permissive license. 72 - 73 - ## Livepeer Network Improvements 74 - 75 - So why should the Livepeer Network participants care about this? Aside from the possibility of Aquareum's success driving traffic to the Livepeer network processing, there are numerous specific protocol upgrades: 76 - 77 - * We'll be packaging up go-livepeer as a library that can be used within other code, dramatically expanding its use cases. This will make it possible to embed Livepeer Network transcoding in any application capable of linking a C library. 78 - * We will be establishing a GitLab instance with hosted CI runners that will be available free of charge to other projects in the ecosystem. This server will also be used to facilitate project management. 79 - * Aquareum will (finally!) give a coherent answer to how the Livepeer protocol handles scaled video distribution — video users run Aquareum nodes. 80 - * We will seek to upgrade the Livepeer wire protocol to utilize C2PA signatures for transcoding. (This is how it would have been doing originally, if the C2PA had existed when the original protocol was developed.) If necessary, we will deliver the actual protocol improvements via an LIP. This will also dramatically increase the utility of Livepeer transcoding. Transcoded segments are presently mostly opaque. With these schema, transcoded Livepeer segments will instead be digital artifacts with a coherent provenance chain, identifying the user, the transcoder, the content, the licensing, and a million other things. 81 - 82 - ## Roadmap 83 - 84 - We have made our roadmap available on the forums; an archived version is also available at [/ipfs/QmPxASKEjVqWnUpVpmbF35Z9WmAiGKT7sQ6ER5HMuUB9Wt](https://ipfs.io/ipfs/QmPxASKEjVqWnUpVpmbF35Z9WmAiGKT7sQ6ER5HMuUB9Wt). 85 - 86 - And one more thing: development will be livestreamed as much as possible — development, code review, project planning will be livestreamed on the Aquareum app and website. We'll be scratching our own itch for a streaming platform while building everything out. We see this as one of the most honest ways to hold ourselves accountable; tune in and see exactly how the treasury funding is being spent! Our livestreaming premiere will be on Tuesday Jul 16 2024 10:00 PDT (12:00 EDT, 17:00 UTC); check the countdown at [aquareum.tv](https://aquareum.tv). 87 - 88 - `.trim();
-7
js/app/app/background.tsx
··· 1 - // earliest part of aquareum's entrypoint. set up message notifications and stuff. 2 - // index.js 3 - import { initPushNotifications } from "./platform"; 4 - 5 - export default async function background() { 6 - await initPushNotifications(); 7 - }
-11
js/app/app/embed/[stream].tsx
··· 1 - import { Player } from "components"; 2 - import { useLocalSearchParams } from "expo-router"; 3 - import { View } from "tamagui"; 4 - 5 - export default function StreamPage() { 6 - const params = useLocalSearchParams(); 7 - if (typeof params.stream !== "string") { 8 - return <View />; 9 - } 10 - return <Player src={params.stream}></Player>; 11 - }
-50
js/app/app/multi/[players].tsx
··· 1 - import { Player } from "components"; 2 - import { PlayerProps } from "components/player/props"; 3 - import { useLocalSearchParams } from "expo-router"; 4 - import { useEffect, useState } from "react"; 5 - import { View, XStack, YStack } from "tamagui"; 6 - 7 - export default function StreamPage() { 8 - const params = useLocalSearchParams(); 9 - if (typeof params.players !== "string") { 10 - return <View />; 11 - } 12 - const [rows, setRows] = useState<Partial<PlayerProps | null>[][]>([]); 13 - useEffect(() => { 14 - let nearestSquareExpo = 1; 15 - const playerProps = JSON.parse( 16 - params.players as string, 17 - ) as Partial<PlayerProps>[]; 18 - while (Math.pow(nearestSquareExpo, 2) < playerProps.length) { 19 - nearestSquareExpo += 1; 20 - } 21 - const rows: Partial<PlayerProps | null>[][] = []; 22 - let idx = 0; 23 - for (let i = 0; i < nearestSquareExpo; i += 1) { 24 - const row: Partial<PlayerProps | null>[] = []; 25 - for (let j = 0; j < nearestSquareExpo; j += 1) { 26 - if (playerProps[idx]) { 27 - row.push(playerProps[idx]); 28 - } else { 29 - row.push(null); 30 - } 31 - idx += 1; 32 - } 33 - rows.push(row); 34 - } 35 - setRows(rows); 36 - }, [params.players]); 37 - return ( 38 - <YStack f={1} fb={0}> 39 - {rows.map((players, i) => ( 40 - <XStack key={i} f={1} fb={0}> 41 - {players.map((props, j) => ( 42 - <View key={j} f={1} fb={0}> 43 - {props === null ? <View /> : <Player {...props}></Player>} 44 - </View> 45 - ))} 46 - </XStack> 47 - ))} 48 - </YStack> 49 - ); 50 - }
-1
js/app/app/platform/index.tsx
··· 1 - export * from "./platform";
js/app/app/platform/platform.android.tsx js/app/hooks/platform/platform.android.tsx
js/app/app/platform/platform.ios.tsx js/app/hooks/platform/platform.ios.tsx
js/app/app/platform/platform.mobile.tsx js/app/hooks/platform/platform.mobile.tsx
-1
js/app/app/platform/platform.tsx
··· 1 - export * from "./platform.web";
js/app/app/platform/platform.web.tsx js/app/hooks/platform/platform.tsx
-10
js/app/app/settings.tsx
··· 1 - import { Settings } from "components"; 2 - import { View } from "tamagui"; 3 - 4 - export default function OptionsScreen() { 5 - return ( 6 - <View flex={1}> 7 - <Settings /> 8 - </View> 9 - ); 10 - }
-11
js/app/app/stream/[stream].tsx
··· 1 - import { Player } from "components"; 2 - import { useLocalSearchParams } from "expo-router"; 3 - import { View } from "tamagui"; 4 - 5 - export default function StreamPage() { 6 - const params = useLocalSearchParams(); 7 - if (typeof params.stream !== "string") { 8 - return <View />; 9 - } 10 - return <Player src={params.stream}></Player>; 11 - }
+3 -1
js/app/app/support.tsx js/app/src/screens/support.tsx
··· 1 - import { View, isWeb } from "tamagui"; 1 + import usePlatform from "hooks/usePlatform"; 2 2 import { useEffect } from "react"; 3 + import { View } from "react-native"; 3 4 4 5 export default function SupportScreen() { 6 + const { isWeb } = usePlatform(); 5 7 if (isWeb) { 6 8 useEffect(() => { 7 9 document.location.href =
+27
js/app/components/aqlink.tsx
··· 1 + import { Link, useNavigation } from "@react-navigation/native"; 2 + import { NavigationProp, ParamListBase } from "@react-navigation/native"; 3 + import usePlatform from "hooks/usePlatform"; 4 + import { Pressable } from "react-native"; 5 + 6 + // Web and native have some disagreements about link styling 7 + // so we have a custom component that handles that 8 + export default function AQLink({ 9 + children, 10 + to, 11 + }: { 12 + children: React.ReactNode; 13 + to: { screen: string; params?: Record<string, string> }; 14 + }) { 15 + const { isWeb } = usePlatform(); 16 + const navigation = useNavigation<NavigationProp<ParamListBase>>(); 17 + 18 + if (isWeb) { 19 + return <Link to={to as any}>{children}</Link>; 20 + } 21 + 22 + return ( 23 + <Pressable onPress={() => navigation.navigate(to.screen, to.params)}> 24 + {children} 25 + </Pressable> 26 + ); 27 + }
+14 -3
js/app/components/player/video-retry.tsx
··· 5 5 props: PlayerProps & { children: React.ReactNode }, 6 6 ) { 7 7 const [resetTime, setResetTime] = useState<number>(Date.now()); 8 + const [retryCount, setRetryCount] = useState(0); 8 9 const isPlaying = props.status === PlayerStatus.PLAYING; 10 + 9 11 useEffect(() => { 10 12 if (isPlaying) { 13 + setRetryCount(0); 11 14 return; 12 15 } 16 + 17 + const baseDelay = 10000; // 10 seconds 18 + const maxDelay = 30000; // 30 seconds 19 + const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay); 20 + 13 21 const handle = setTimeout(() => { 14 - // you've had long enough. try again! 22 + // console.log(`retrying (attempt ${retryCount + 1}, delay: ${delay}ms)`); 15 23 setResetTime(Date.now()); 16 - }, 5000); 24 + setRetryCount((prev) => prev + 1); 25 + }, delay); 26 + 17 27 return () => clearTimeout(handle); 18 - }, [isPlaying]); 28 + }, [isPlaying, resetTime, retryCount]); 29 + 19 30 return <React.Fragment key={resetTime}>{props.children}</React.Fragment>; 20 31 }
+1
js/app/components/player/video.tsx
··· 56 56 }; 57 57 58 58 useEffect(() => { 59 + console.log("video mounted"); 59 60 return () => { 60 61 props.setStatus(PlayerStatus.START); 61 62 };
+15 -2
js/app/components/provider/provider.native.tsx
··· 1 + // this comment is here to stop auto-alphabetizing imports lol 2 + import { LinkingOptions } from "@react-navigation/native"; 1 3 import React from "react"; 4 + import { GestureHandlerRootView } from "react-native-gesture-handler"; 2 5 import SharedProvider from "./provider.shared"; 3 6 4 - export default function Provider({ children }: { children: React.ReactNode }) { 5 - return <SharedProvider>{children}</SharedProvider>; 7 + export default function Provider({ 8 + children, 9 + linking, 10 + }: { 11 + children: React.ReactNode; 12 + linking: LinkingOptions<ReactNavigation.RootParamList>; 13 + }) { 14 + return ( 15 + <GestureHandlerRootView style={{ flex: 1 }}> 16 + <SharedProvider linking={linking}>{children}</SharedProvider> 17 + </GestureHandlerRootView> 18 + ); 6 19 }
+53 -24
js/app/components/provider/provider.shared.tsx
··· 1 + import { 2 + DarkTheme, 3 + LinkingOptions, 4 + NavigationContainer, 5 + } from "@react-navigation/native"; 1 6 import { ToastProvider, ToastViewport } from "@tamagui/toast"; 2 - import { CurrentToast } from "app/CurrentToast"; 7 + import { useFonts } from "expo-font"; 8 + import { AquareumProvider } from "hooks/useAquareumNode"; 3 9 import React from "react"; 4 - import { TamaguiProvider, PortalProvider } from "tamagui"; 10 + import { PortalProvider, TamaguiProvider } from "tamagui"; 5 11 import config from "tamagui.config"; 6 - import { AquareumProvider } from "hooks/useAquareumNode"; 12 + import { CurrentToast } from "./CurrentToast"; 7 13 8 - export default function Provider({ children }: { children: React.ReactNode }) { 14 + export default function Provider({ 15 + children, 16 + linking, 17 + }: { 18 + children: React.ReactNode; 19 + linking: LinkingOptions<ReactNavigation.RootParamList>; 20 + }) { 9 21 return ( 10 - <AquareumProvider> 11 - <TamaguiProvider config={config} defaultTheme={"dark"}> 12 - <PortalProvider> 13 - <ToastProvider 14 - swipeDirection="vertical" 15 - duration={6000} 16 - native={ 17 - [ 18 - /* uncomment the next line to do native toasts on mobile. NOTE: it'll require you making a dev build and won't work with Expo Go */ 19 - // 'mobile' 20 - ] 21 - } 22 - > 23 - {children} 24 - <CurrentToast /> 25 - <ToastViewport name="default" top="$8" left={0} right={0} /> 26 - </ToastProvider> 27 - </PortalProvider> 28 - </TamaguiProvider> 29 - </AquareumProvider> 22 + <TamaguiProvider config={config} defaultTheme={"dark"}> 23 + <NavigationContainer theme={DarkTheme} linking={linking}> 24 + <AquareumProvider> 25 + <PortalProvider> 26 + <ToastProvider 27 + swipeDirection="vertical" 28 + duration={6000} 29 + native={ 30 + [ 31 + /* uncomment the next line to do native toasts on mobile. NOTE: it'll require you making a dev build and won't work with Expo Go */ 32 + // 'mobile' 33 + ] 34 + } 35 + > 36 + <FontProvider>{children}</FontProvider> 37 + <CurrentToast /> 38 + <ToastViewport name="default" top="$8" left={0} right={0} /> 39 + </ToastProvider> 40 + </PortalProvider> 41 + </AquareumProvider> 42 + </NavigationContainer> 43 + </TamaguiProvider> 30 44 ); 31 45 } 46 + 47 + export const FontProvider = ({ children }: { children: React.ReactNode }) => { 48 + const [fontLoaded, fontError] = useFonts({ 49 + "FiraCode-Light": require("../../assets/fonts/FiraCode-Light.ttf"), 50 + "FiraCode-Medium": require("../../assets/fonts/FiraCode-Medium.ttf"), 51 + "FiraCode-Bold": require("../../assets/fonts/FiraCode-Bold.ttf"), 52 + "FiraSans-Medium": require("../../assets/fonts/FiraSans-Medium.ttf"), 53 + }); 54 + 55 + if (!fontLoaded && !fontError) { 56 + return null; 57 + } 58 + 59 + return <>{children}</>; 60 + };
+9 -2
js/app/components/provider/provider.tsx
··· 7 7 import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; 8 8 import { View, Text } from "tamagui"; 9 9 import SharedProvider from "./provider.shared"; 10 + import { LinkingOptions } from "@react-navigation/native"; 10 11 11 12 const queryClient = new QueryClient(); 12 13 ··· 18 19 ssr: true, // If your dApp uses server side rendering (SSR) 19 20 }); 20 21 21 - export default function Provider({ children }: { children: React.ReactNode }) { 22 + export default function Provider({ 23 + children, 24 + linking, 25 + }: { 26 + children: React.ReactNode; 27 + linking: LinkingOptions<ReactNavigation.RootParamList>; 28 + }) { 22 29 return ( 23 - <SharedProvider> 30 + <SharedProvider linking={linking}> 24 31 <WagmiProvider config={config}> 25 32 <QueryClientProvider client={queryClient}> 26 33 <RainbowKitProvider coolMode={true}>
+38 -13
js/app/components/stream-list/stream-list.tsx
··· 1 + import { Link, useNavigation } from "@react-navigation/native"; 2 + import AQLink from "components/aqlink"; 1 3 import ErrorBox from "components/error/error"; 2 4 import Loading from "components/loading/loading"; 3 - import { Link } from "expo-router"; 5 + import { formatAddress } from "hooks/textUtils"; 4 6 import useAquareumNode from "hooks/useAquareumNode"; 5 7 import { useEffect, useState } from "react"; 6 - import { Pressable } from "react-native"; 7 - import { ScrollView, Text, Image, View, H2, H6 } from "tamagui"; 8 + import { Pressable, RefreshControl } from "react-native"; 9 + import { H6, Image, ScrollView, ScrollViewProps, View, YStack } from "tamagui"; 8 10 9 11 type Segment = { 10 12 id: string; ··· 13 15 endTime: string; 14 16 }; 15 17 16 - export default function StreamList() { 18 + export default function StreamList({ 19 + contentContainerStyle = {}, 20 + }: { 21 + contentContainerStyle?: Exclude< 22 + ScrollViewProps["contentContainerStyle"], 23 + string 24 + >; 25 + }) { 17 26 const [streams, setStreams] = useState<Segment[]>([]); 18 27 const [error, setError] = useState<boolean>(false); 19 28 const [loading, setLoading] = useState<boolean>(false); 20 29 const [retryTime, setRetryTime] = useState<number>(Date.now()); 21 30 const { url } = useAquareumNode(); 31 + const navigation = useNavigation(); 22 32 useEffect(() => { 23 33 setError(false); 24 34 setLoading(true); ··· 48 58 return <ErrorBox onRetry={() => setRetryTime(Date.now())} />; 49 59 } 50 60 return ( 51 - <ScrollView contentContainerStyle={{ alignItems: "center" }}> 61 + <ScrollView 62 + contentContainerStyle={{ 63 + alignItems: "stretch", 64 + 65 + ...contentContainerStyle, 66 + }} 67 + refreshControl={ 68 + <RefreshControl 69 + refreshing={loading} 70 + onRefresh={() => setRetryTime(Date.now())} 71 + /> 72 + } 73 + > 52 74 {streams.map((seg) => ( 53 - <Link asChild key={seg.user} href={`/stream/${seg.user}`}> 54 - <Pressable> 55 - <View key={seg.user}> 75 + <View flex={1}> 76 + <AQLink to={{ screen: "Stream", params: { user: seg.user } }}> 77 + <YStack f={1} alignItems="center"> 56 78 <Image 57 - height={200} 79 + f={1} 80 + aspectRatio={16 / 9} 81 + maxWidth={400} 82 + width="100%" 58 83 src={`${url}/api/playback/${seg.user}/stream.jpg`} 59 84 resizeMode="contain" 60 85 objectFit="contain" 61 86 /> 62 - <H6>{seg.user}</H6> 63 - </View> 64 - </Pressable> 65 - </Link> 87 + <H6>{formatAddress(seg.user)}</H6> 88 + </YStack> 89 + </AQLink> 90 + </View> 66 91 ))} 67 92 </ScrollView> 68 93 );
+3
js/app/hooks/platform/index.tsx
··· 1 + import { initPushNotifications, topSafeHeight } from "./platform"; 2 + 3 + export { initPushNotifications, topSafeHeight };
+3
js/app/hooks/textUtils.tsx
··· 1 + export function formatAddress(address: string) { 2 + return address.slice(0, 6) + "..." + address.slice(-4); 3 + }
+3
js/app/hooks/useAquareumNode.tsx
··· 2 2 import { isWeb } from "tamagui"; 3 3 4 4 let DEFAULT_URL = process.env.EXPO_PUBLIC_AQUAREUM_URL; 5 + console.log({ 6 + EXPO_PUBLIC_WEB_TRY_LOCAL: process.env.EXPO_PUBLIC_WEB_TRY_LOCAL, 7 + }); 5 8 if (isWeb && process.env.EXPO_PUBLIC_WEB_TRY_LOCAL === "true") { 6 9 try { 7 10 DEFAULT_URL = `${window.location.protocol}//${window.location.host}`;
+3
js/app/hooks/usePlatform.native.tsx
··· 1 1 import { Platform } from "react-native"; 2 + import { initPushNotifications, topSafeHeight } from "./platform"; 2 3 import { IsPlatform } from "./usePlatform.shared"; 3 4 4 5 export default function usePlatform(): IsPlatform { 5 6 return { 7 + initPushNotifications, 8 + topSafeHeight, 6 9 isNative: true, 7 10 isIOS: Platform.OS === "ios", 8 11 isAndroid: Platform.OS === "android",
+3
js/app/hooks/usePlatform.shared.tsx
··· 13 13 isChrome: boolean; 14 14 // don't rely on this! just for defaults 15 15 isFirefox: boolean; 16 + 17 + initPushNotifications: () => Promise<void>; 18 + topSafeHeight: () => number; 16 19 };
+3
js/app/hooks/usePlatform.tsx
··· 1 1 import { IsPlatform } from "./usePlatform.shared"; 2 2 // it's only for setting defaults, i promise! 3 3 import uaParser from "ua-parser-js"; 4 + import { initPushNotifications, topSafeHeight } from "./platform"; 4 5 5 6 function supportsHLS() { 6 7 var video = document.createElement("video"); ··· 18 19 } 19 20 const electron = typeof window["AQ_ELECTRON"] !== "undefined"; 20 21 return { 22 + initPushNotifications, 23 + topSafeHeight, 21 24 isNative: false, 22 25 isIOS: false, 23 26 isAndroid: false,
+10 -6
js/app/package.json
··· 1 1 { 2 2 "name": "aquareum", 3 - "main": "expo-router/entry", 3 + "main": "./src/entrypoint.tsx", 4 4 "version": "0.2.2", 5 + "runtimeVersion": "0.2.2", 5 6 "scripts": { 6 7 "upgrade:tamagui": "yarn up '*tamagui*'@latest '@tamagui/*'@latest", 7 8 "upgrade:tamagui:canary": "yarn up '*tamagui*'@canary '@tamagui/*'@canary", ··· 27 28 "@rainbow-me/rainbowkit": "2", 28 29 "@react-native-firebase/app": "^20.3.0", 29 30 "@react-native-firebase/messaging": "^20.3.0", 30 - "@react-navigation/native": "^6.1.17", 31 + "@react-navigation/bottom-tabs": "^6.6.1", 32 + "@react-navigation/drawer": "^6.7.2", 33 + "@react-navigation/native": "^6.1.18", 34 + "@react-navigation/native-stack": "^6.11.0", 31 35 "@tamagui/config": "^1.102.3", 32 36 "@tamagui/lucide-icons": "^1.102.3", 33 37 "@tamagui/toast": "^1.102.3", ··· 41 45 "expo-font": "~12.0.9", 42 46 "expo-linking": "~6.3.1", 43 47 "expo-notifications": "~0.28.10", 44 - "expo-router": "~3.5.18", 45 48 "expo-splash-screen": "~0.27.5", 46 49 "expo-status-bar": "^1.12.1", 47 50 "expo-system-ui": "~3.0.7", ··· 52 55 "react": "18.3.1", 53 56 "react-dom": "18.3.1", 54 57 "react-native": "0.74.3", 58 + "react-native-gesture-handler": "~2.16.1", 55 59 "react-native-markdown-display": "^7.0.2", 56 - "react-native-reanimated": "~3.14.0", 57 - "react-native-safe-area-context": "4.10.8", 58 - "react-native-screens": "~3.32.0", 60 + "react-native-reanimated": "~3.10.1", 61 + "react-native-safe-area-context": "4.10.5", 62 + "react-native-screens": "3.31.1", 59 63 "react-native-svg": "15.4.0", 60 64 "react-native-web": "^0.19.12", 61 65 "react-native-webview": "13.10.5",
+36
js/app/public/index.html
··· 1 + <!doctype html> 2 + <html lang="%LANG_ISO_CODE%"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta httpEquiv="X-UA-Compatible" content="IE=edge" /> 6 + <meta 7 + name="viewport" 8 + content="width=device-width, initial-scale=1, shrink-to-fit=no" 9 + /> 10 + <title>Aquareum</title> 11 + <style id="expo-reset"> 12 + html, 13 + body { 14 + height: 100%; 15 + } 16 + body { 17 + overflow: hidden; 18 + } 19 + #root { 20 + display: flex; 21 + height: 100%; 22 + flex: 1; 23 + } 24 + </style> 25 + <style> 26 + html { 27 + background-color: black; 28 + } 29 + </style> 30 + </head> 31 + 32 + <body> 33 + <noscript> You need to enable JavaScript to run this app. </noscript> 34 + <div id="root"></div> 35 + </body> 36 + </html>
+13
js/app/public/serve.json
··· 1 + { 2 + "headers": [ 3 + { 4 + "source": "static/**/*.js", 5 + "headers": [ 6 + { 7 + "key": "Cache-Control", 8 + "value": "public, max-age=31536000, immutable" 9 + } 10 + ] 11 + } 12 + ] 13 + }
+8
js/app/src/entrypoint.tsx
··· 1 + // Don't add anything to this file! It needs to be minimal so that 2 + // hot module reloading works properly on web. 3 + 4 + import "@expo/metro-runtime"; 5 + import { registerRootComponent } from "expo"; 6 + import Router from "./router"; 7 + 8 + registerRootComponent(Router);
+198
js/app/src/router.tsx
··· 1 + import "@expo/metro-runtime"; 2 + import { createDrawerNavigator } from "@react-navigation/drawer"; 3 + import { 4 + CommonActions, 5 + DrawerActions, 6 + LinkingOptions, 7 + useNavigation, 8 + } from "@react-navigation/native"; 9 + import { createNativeStackNavigator } from "@react-navigation/native-stack"; 10 + import { 11 + ArrowLeft, 12 + Home, 13 + LockKeyhole, 14 + Menu, 15 + Settings as SettingsIcon, 16 + } from "@tamagui/lucide-icons"; 17 + import { Provider, Settings } from "components"; 18 + import Admin from "components/admin"; 19 + import StreamList from "components/stream-list/stream-list"; 20 + import usePlatform from "hooks/usePlatform"; 21 + import { useEffect } from "react"; 22 + import { Pressable } from "react-native"; 23 + import { useTheme, View } from "tamagui"; 24 + import MultiScreen from "./screens/multi"; 25 + import StreamScreen from "./screens/stream"; 26 + import SupportScreen from "./screens/support"; 27 + 28 + function HomeScreen() { 29 + return ( 30 + <View f={1}> 31 + <StreamList contentContainerStyle={{ paddingTop: "$3" }}></StreamList> 32 + </View> 33 + ); 34 + } 35 + 36 + const Stack = createNativeStackNavigator(); 37 + 38 + const linking: LinkingOptions<ReactNavigation.RootParamList> = { 39 + prefixes: ["tv.aquareum://", "tv.aquareum.dev://"], 40 + config: { 41 + screens: { 42 + Home: { 43 + screens: { 44 + StreamList: "", 45 + Stream: { 46 + path: "stream/:user", 47 + }, 48 + }, 49 + }, 50 + Multi: "multi/:config", 51 + Admin: "admin", 52 + Support: "support", 53 + Settings: "settings", 54 + }, 55 + }, 56 + }; 57 + 58 + const Drawer = createDrawerNavigator(); 59 + 60 + const NavigationButton = ({ canGoBack }: { canGoBack?: boolean }) => { 61 + const navigation = useNavigation(); 62 + return ( 63 + <Pressable 64 + style={{ padding: 10 }} 65 + onPress={() => { 66 + if (canGoBack) { 67 + navigation.goBack(); 68 + } else { 69 + navigation.dispatch(DrawerActions.toggleDrawer()); 70 + } 71 + }} 72 + > 73 + {canGoBack ? <ArrowLeft /> : <Menu />} 74 + </Pressable> 75 + ); 76 + }; 77 + 78 + export default function Router() { 79 + const { initPushNotifications, isWeb, isElectron } = usePlatform(); 80 + useEffect(() => { 81 + initPushNotifications(); 82 + }, []); 83 + if (isWeb && !isElectron) { 84 + linking.prefixes.push(document.location.origin); 85 + } 86 + return ( 87 + <Provider linking={linking}> 88 + <AquareumDrawer /> 89 + </Provider> 90 + ); 91 + } 92 + 93 + export function AquareumDrawer() { 94 + const theme = useTheme(); 95 + const { isWeb } = usePlatform(); 96 + const navigation = useNavigation(); 97 + return ( 98 + <Drawer.Navigator 99 + initialRouteName="Home" 100 + screenOptions={{ 101 + headerLeft: () => <NavigationButton />, 102 + drawerActiveTintColor: theme.accentColor.val, 103 + headerStyle: {}, 104 + }} 105 + > 106 + <Drawer.Screen 107 + name="Home" 108 + component={MainTab} 109 + options={{ 110 + drawerIcon: () => <Home />, 111 + headerTitle: "Aquareum", 112 + headerShown: isWeb, 113 + }} 114 + listeners={{ 115 + drawerItemPress: (e) => { 116 + e.preventDefault(); 117 + navigation.dispatch( 118 + CommonActions.reset({ 119 + index: 0, 120 + routes: [ 121 + { 122 + name: "Home", 123 + state: { 124 + routes: [{ name: "StreamList" }], 125 + }, 126 + }, 127 + ], 128 + }), 129 + ); 130 + }, 131 + }} 132 + /> 133 + <Drawer.Screen 134 + name="Settings" 135 + component={Settings} 136 + options={{ drawerIcon: () => <SettingsIcon /> }} 137 + /> 138 + {isWeb && ( 139 + <Drawer.Screen 140 + name="Admin" 141 + component={Admin} 142 + options={{ 143 + drawerIcon: () => <LockKeyhole />, 144 + drawerLabel: () => null, 145 + drawerItemStyle: { display: "none" }, 146 + }} 147 + /> 148 + )} 149 + <Drawer.Screen 150 + name="Multi" 151 + component={MultiScreen} 152 + options={{ 153 + drawerLabel: () => null, 154 + drawerItemStyle: { display: "none" }, 155 + }} 156 + /> 157 + <Drawer.Screen 158 + name="Support" 159 + component={SupportScreen} 160 + options={{ 161 + drawerLabel: () => null, 162 + drawerItemStyle: { display: "none" }, 163 + }} 164 + /> 165 + </Drawer.Navigator> 166 + ); 167 + } 168 + 169 + const MainTab = () => { 170 + const theme = useTheme(); 171 + const { isWeb } = usePlatform(); 172 + return ( 173 + // <SafeAreaView style={{ flex: 1, backgroundColor: theme.background.val }}> 174 + <Stack.Navigator 175 + initialRouteName="StreamList" 176 + screenOptions={{ 177 + headerLeft: ({ canGoBack }) => ( 178 + <NavigationButton canGoBack={canGoBack} /> 179 + ), 180 + headerShown: !isWeb, 181 + }} 182 + > 183 + <Stack.Screen 184 + name="StreamList" 185 + component={HomeScreen} 186 + options={{ headerTitle: "Aquareum" }} 187 + /> 188 + <Stack.Screen 189 + name="Stream" 190 + component={StreamScreen} 191 + options={{ 192 + headerTitle: "Stream", 193 + }} 194 + /> 195 + </Stack.Navigator> 196 + // </SafeAreaView> 197 + ); 198 + };
+58
js/app/src/screens/multi.tsx
··· 1 + import { Player } from "components"; 2 + import { PlayerProps } from "components/player/props"; 3 + import { useEffect, useState } from "react"; 4 + import { Text, View, XStack, YStack } from "tamagui"; 5 + 6 + export default function MultiScreen({ route }) { 7 + const config = route.params?.config; 8 + if (typeof config !== "string") { 9 + return <View />; 10 + } 11 + 12 + const [rows, setRows] = useState<Partial<PlayerProps | null>[][]>([]); 13 + const [error, setError] = useState<string | null>(null); 14 + useEffect(() => { 15 + try { 16 + let nearestSquareExpo = 1; 17 + const playerProps = JSON.parse( 18 + config as string, 19 + ) as Partial<PlayerProps>[]; 20 + while (Math.pow(nearestSquareExpo, 2) < playerProps.length) { 21 + nearestSquareExpo += 1; 22 + } 23 + const rows: Partial<PlayerProps | null>[][] = []; 24 + let idx = 0; 25 + for (let i = 0; i < nearestSquareExpo; i += 1) { 26 + const row: Partial<PlayerProps | null>[] = []; 27 + for (let j = 0; j < nearestSquareExpo; j += 1) { 28 + if (playerProps[idx]) { 29 + row.push(playerProps[idx]); 30 + } else { 31 + row.push(null); 32 + } 33 + idx += 1; 34 + } 35 + rows.push(row); 36 + } 37 + setRows(rows); 38 + } catch (e) { 39 + setError(e.message); 40 + } 41 + }, [config]); 42 + if (error) { 43 + return <Text>{error}</Text>; 44 + } 45 + return ( 46 + <YStack f={1} fb={0}> 47 + {rows.map((players, i) => ( 48 + <XStack key={i} f={1} fb={0}> 49 + {players.map((props, j) => ( 50 + <View key={j} f={1} fb={0}> 51 + {props === null ? <View /> : <Player {...props}></Player>} 52 + </View> 53 + ))} 54 + </XStack> 55 + ))} 56 + </YStack> 57 + ); 58 + }
js/app/src/screens/settings.tsx

This is a binary file and will not be displayed.

+6
js/app/src/screens/stream.tsx
··· 1 + import { Player } from "components/player/player"; 2 + 3 + export default function StreamScreen({ route }) { 4 + const { user } = route.params; 5 + return <Player src={user} />; 6 + }
+16 -3
js/desktop/src/index.ts
··· 1 - import { app, BrowserWindow, dialog } from "electron"; 1 + import { app, BrowserWindow, dialog, globalShortcut } from "electron"; 2 2 import { parseArgs } from "node:util"; 3 3 import { resolve } from "path"; 4 4 import "source-map-support/register"; ··· 94 94 } 95 95 const mainWindow = await makeWindow(); 96 96 97 + globalShortcut.register("CommandOrControl+Shift+I", () => { 98 + mainWindow.webContents.toggleDevTools(); 99 + }); 100 + 97 101 let startPath; 98 102 if (nodeFrontend) { 99 103 startPath = `${loadAddr}${args.path}`; ··· 120 124 try { 121 125 const mainWindow = await makeWindow(); 122 126 127 + globalShortcut.register("CommandOrControl+Shift+I", () => { 128 + mainWindow.webContents.toggleDevTools(); 129 + }); 130 + 123 131 const testId = uuidv7(); 124 132 const definitions = [ 125 133 { ··· 143 151 })); 144 152 const enc = encodeURIComponent(JSON.stringify(tests)); 145 153 154 + console.log(`http://localhost:38081/multi/${enc}`); 155 + 156 + let load; 146 157 if (nodeFrontend) { 147 - mainWindow.loadURL(`${addr}/multi/${enc}`); 158 + load = `${addr}/multi/${enc}`; 148 159 } else { 149 - mainWindow.loadURL(`http://localhost:38081/multi/${enc}`); 160 + load = `http://localhost:38081/multi/${enc}`; 150 161 } 162 + console.log(`opening ${load}`); 163 + mainWindow.loadURL(load); 151 164 152 165 let foundThumbnail = false; 153 166 const interval = setInterval(async () => {
+3 -15
pkg/api/app-updates.go
··· 224 224 return nil, err 225 225 } 226 226 227 - file2, err := fs.Open("expoConfig.json") 228 - if err != nil { 229 - return nil, err 230 - } 231 - bs2, err := io.ReadAll(file2) 232 - if err != nil { 233 - return nil, err 234 - } 235 - extra := map[string]any{} 236 - err = json.Unmarshal(bs2, &extra) 237 - if err != nil { 238 - return nil, err 239 - } 227 + extra, err := app.PackageJSON() 240 228 241 229 rt, ok := extra["runtimeVersion"] 242 230 if !ok { 243 - return nil, fmt.Errorf("expoConfig.json missing runtimeVersion") 231 + return nil, fmt.Errorf("package.json missing runtimeVersion") 244 232 } 245 233 runtimeVersion, ok := rt.(string) 246 234 if !ok { 247 - return nil, fmt.Errorf("expoConfig.json has runtimeVersion that's not a string") 235 + return nil, fmt.Errorf("package.json has runtimeVersion that's not a string") 248 236 } 249 237 250 238 var privateKey *rsa.PrivateKey
+46 -3
pkg/api/playback.go
··· 6 6 "fmt" 7 7 "io" 8 8 "net/http" 9 + "os" 9 10 "path/filepath" 10 11 "strconv" 11 12 "strings" ··· 104 105 } 105 106 } 106 107 107 - func (a *AquareumAPI) HandleHLSPlayback(ctx context.Context) httprouter.Handle { 108 + var epoch = time.Unix(0, 0).Format(time.RFC1123) 109 + 110 + var noCacheHeaders = map[string]string{ 111 + "Expires": epoch, 112 + "Cache-Control": "no-cache, private, max-age=0", 113 + "Pragma": "no-cache", 114 + "X-Accel-Expires": "0", 115 + } 116 + 117 + var etagHeaders = []string{ 118 + "ETag", 119 + "If-Modified-Since", 120 + "If-Match", 121 + "If-None-Match", 122 + "If-Range", 123 + "If-Unmodified-Since", 124 + } 125 + 126 + func NoCache(h httprouter.Handle) httprouter.Handle { 108 127 return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 128 + // Delete any ETag headers that may have been set 129 + for _, v := range etagHeaders { 130 + if r.Header.Get(v) != "" { 131 + r.Header.Del(v) 132 + } 133 + } 134 + 135 + // Set our NoCache headers 136 + for k, v := range noCacheHeaders { 137 + w.Header().Set(k, v) 138 + } 139 + 140 + h(w, r, p) 141 + } 142 + } 143 + 144 + func (a *AquareumAPI) HandleHLSPlayback(ctx context.Context) httprouter.Handle { 145 + return NoCache(func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 109 146 user := p.ByName("user") 110 147 if user == "" { 111 148 errors.WriteHTTPBadRequest(w, "user required", nil) ··· 124 161 } 125 162 dir := getDir() 126 163 fullpath := filepath.Join(dir, file) 127 - http.ServeFile(w, r, fullpath) 128 - } 164 + f, err := os.Open(fullpath) 165 + if err != nil { 166 + errors.WriteHTTPInternalServerError(w, "could not open file", err) 167 + return 168 + } 169 + defer f.Close() 170 + http.ServeContent(w, r, file, time.Now(), f) 171 + }) 129 172 } 130 173 131 174 func (a *AquareumAPI) HandleThumbnailPlayback(ctx context.Context) httprouter.Handle {
+28 -1
pkg/cmd/aquareum.go
··· 40 40 41 41 // parse the CLI and fire up an aquareum node! 42 42 func start(build *config.BuildFlags, platformJobs []jobFunc) error { 43 + selfTest := len(os.Args) > 1 && os.Args[1] == "self-test" 44 + err := media.RunSelfTest(context.Background()) 45 + if err != nil { 46 + if selfTest { 47 + fmt.Println(err.Error()) 48 + os.Exit(1) 49 + } else { 50 + retryCount, _ := strconv.Atoi(os.Getenv("AQUAREUM_SELFTEST_RETRY")) 51 + if retryCount >= 3 { 52 + log.Error(context.Background(), "gstreamer self-test failed 3 times, giving up", "error", err) 53 + return err 54 + } 55 + log.Log(context.Background(), "error in gstreamer self-test, attempting recovery", "error", err, "retry", retryCount+1) 56 + os.Setenv("AQUAREUM_SELFTEST_RETRY", strconv.Itoa(retryCount+1)) 57 + err := syscall.Exec(os.Args[0], os.Args[1:], os.Environ()) 58 + if err != nil { 59 + log.Error(context.Background(), "error in gstreamer self-test, could not restart", "error", err) 60 + return err 61 + } 62 + panic("invalid code path: exec succeeded but we're still here???") 63 + } 64 + } 65 + if selfTest { 66 + fmt.Println("self-test successful!") 67 + os.Exit(0) 68 + } 69 + 43 70 if len(os.Args) > 1 && os.Args[1] == "stream" { 44 71 if len(os.Args) != 3 { 45 72 fmt.Println("usage: aquareum stream [user]") ··· 101 128 fs.IntVar(&cli.MistHTTPPort, "mist-http-port", 18080, "MistServer HTTP port (internal use only)") 102 129 } 103 130 104 - err := cli.Parse( 131 + err = cli.Parse( 105 132 fs, os.Args[1:], 106 133 ) 107 134 if err != nil {
+109 -331
yarn.lock
··· 1923 1923 languageName: node 1924 1924 linkType: hard 1925 1925 1926 - "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.23.2": 1926 + "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.23.2": 1927 1927 version: 7.24.7 1928 1928 resolution: "@babel/runtime@npm:7.24.7" 1929 1929 dependencies: ··· 2030 2030 dependencies: 2031 2031 "@jridgewell/trace-mapping": "npm:0.3.9" 2032 2032 checksum: 10/b6e38a1712fab242c86a241c229cf562195aad985d0564bd352ac404be583029e89e93028ffd2c251d2c407ecac5fb0cbdca94a2d5c10f29ac806ede0508b3ff 2033 + languageName: node 2034 + linkType: hard 2035 + 2036 + "@egjs/hammerjs@npm:^2.0.17": 2037 + version: 2.0.17 2038 + resolution: "@egjs/hammerjs@npm:2.0.17" 2039 + dependencies: 2040 + "@types/hammerjs": "npm:^2.0.36" 2041 + checksum: 10/f695129d45edfcfd6c5f2d1d36186da36ffade013991972ce23721a6b7ad7f214ce282abc4023e3f6b63062620852a63e897b523f247804afc7acd188fee9d9d 2033 2042 languageName: node 2034 2043 linkType: hard 2035 2044 ··· 3148 3157 languageName: node 3149 3158 linkType: hard 3150 3159 3151 - "@expo/metro-runtime@npm:3.2.1, @expo/metro-runtime@npm:~3.2.1": 3160 + "@expo/metro-runtime@npm:~3.2.1": 3152 3161 version: 3.2.1 3153 3162 resolution: "@expo/metro-runtime@npm:3.2.1" 3154 3163 peerDependencies: ··· 3259 3268 version: 1.0.0 3260 3269 resolution: "@expo/sdk-runtime-versions@npm:1.0.0" 3261 3270 checksum: 10/0942d5a356f590e8dc795761456cc48b3e2d6a38ad2a02d6774efcdc5a70424e05623b4e3e5d2fec0cdc30f40dde05c14391c781607eed3971bf8676518bfd9d 3262 - languageName: node 3263 - linkType: hard 3264 - 3265 - "@expo/server@npm:^0.4.0": 3266 - version: 0.4.3 3267 - resolution: "@expo/server@npm:0.4.3" 3268 - dependencies: 3269 - "@remix-run/node": "npm:^2.7.2" 3270 - abort-controller: "npm:^3.0.0" 3271 - debug: "npm:^4.3.4" 3272 - source-map-support: "npm:~0.5.21" 3273 - checksum: 10/3965a05cd05efcc241a5e3ab8dfc33853ba64f4c4995de68c5c71f80638fccc01659fad8a750580c222a8572a09cbfa80470d2308f9a3e39bf32cecb29f5fdb0 3274 3271 languageName: node 3275 3272 linkType: hard 3276 3273 ··· 5498 5495 languageName: node 5499 5496 linkType: hard 5500 5497 5501 - "@radix-ui/react-compose-refs@npm:1.0.0": 5502 - version: 1.0.0 5503 - resolution: "@radix-ui/react-compose-refs@npm:1.0.0" 5504 - dependencies: 5505 - "@babel/runtime": "npm:^7.13.10" 5506 - peerDependencies: 5507 - react: ^16.8 || ^17.0 || ^18.0 5508 - checksum: 10/fb98be2e275a1a758ccac647780ff5b04be8dcf25dcea1592db3b691fecf719c4c0700126da605b2f512dd89caa111352b9fad59528d736b4e0e9a0e134a74a1 5509 - languageName: node 5510 - linkType: hard 5511 - 5512 - "@radix-ui/react-slot@npm:1.0.1": 5513 - version: 1.0.1 5514 - resolution: "@radix-ui/react-slot@npm:1.0.1" 5515 - dependencies: 5516 - "@babel/runtime": "npm:^7.13.10" 5517 - "@radix-ui/react-compose-refs": "npm:1.0.0" 5518 - peerDependencies: 5519 - react: ^16.8 || ^17.0 || ^18.0 5520 - checksum: 10/b00fc6ec54a20785263540d9e4a0e3a13d9bc54d7af49b64f6a268eba4a6560c291bd95bbaa7cf7609fdf6fd0ebae54605bb01313de3fa180b06f2a321e9a3b4 5521 - languageName: node 5522 - linkType: hard 5523 - 5524 5498 "@rainbow-me/rainbowkit@npm:2": 5525 5499 version: 2.1.3 5526 5500 resolution: "@rainbow-me/rainbowkit@npm:2.1.3" ··· 5953 5927 languageName: node 5954 5928 linkType: hard 5955 5929 5956 - "@react-navigation/bottom-tabs@npm:~6.5.7": 5957 - version: 6.5.20 5958 - resolution: "@react-navigation/bottom-tabs@npm:6.5.20" 5930 + "@react-navigation/bottom-tabs@npm:^6.6.1": 5931 + version: 6.6.1 5932 + resolution: "@react-navigation/bottom-tabs@npm:6.6.1" 5959 5933 dependencies: 5960 - "@react-navigation/elements": "npm:^1.3.30" 5934 + "@react-navigation/elements": "npm:^1.3.31" 5961 5935 color: "npm:^4.2.3" 5962 5936 warn-once: "npm:^0.1.0" 5963 5937 peerDependencies: ··· 5966 5940 react-native: "*" 5967 5941 react-native-safe-area-context: ">= 3.0.0" 5968 5942 react-native-screens: ">= 3.0.0" 5969 - checksum: 10/327c5a6706a5e990295aa56e3ebda96b196ab67f9edbaa6eb6b80adbe131dddbb1eb564f95c6c0003bfd1e28e4040315a0a7cfd22bd7a79230b41ed6f9ba276d 5943 + checksum: 10/572f67c1ea26ac52a0c599064957ec6ac5cd0eee810d2e3cd5b4f4beb1016fbd5c5e8eb54c4d62836e1afe90c5ef922b31a288f41e0c442123dcfc417aa8fd7f 5970 5944 languageName: node 5971 5945 linkType: hard 5972 5946 5973 - "@react-navigation/core@npm:^6.4.16": 5974 - version: 6.4.16 5975 - resolution: "@react-navigation/core@npm:6.4.16" 5947 + "@react-navigation/core@npm:^6.4.17": 5948 + version: 6.4.17 5949 + resolution: "@react-navigation/core@npm:6.4.17" 5976 5950 dependencies: 5977 5951 "@react-navigation/routers": "npm:^6.1.9" 5978 5952 escape-string-regexp: "npm:^4.0.0" 5979 5953 nanoid: "npm:^3.1.23" 5980 5954 query-string: "npm:^7.1.3" 5981 5955 react-is: "npm:^16.13.0" 5982 - use-latest-callback: "npm:^0.1.9" 5956 + use-latest-callback: "npm:^0.2.1" 5983 5957 peerDependencies: 5984 5958 react: "*" 5985 - checksum: 10/1b58f1566c55412247f06c7a2c769ac588b595a75dc81945dc5b90d7b371fbcf982c16327adb956733467d0bbdc7a93fe3aedce0a8246dda653d07b239e727b0 5959 + checksum: 10/481470361c7dd638d8af513ca559265829e8de5a2ff18c207d8d1c9e2d65606318061ffe369afbccfea3c6d027d38ad539ae5bae8863d9cedd8eaeafeb18426c 5986 5960 languageName: node 5987 5961 linkType: hard 5988 5962 5989 - "@react-navigation/elements@npm:^1.3.30": 5990 - version: 1.3.30 5991 - resolution: "@react-navigation/elements@npm:1.3.30" 5963 + "@react-navigation/drawer@npm:^6.7.2": 5964 + version: 6.7.2 5965 + resolution: "@react-navigation/drawer@npm:6.7.2" 5966 + dependencies: 5967 + "@react-navigation/elements": "npm:^1.3.31" 5968 + color: "npm:^4.2.3" 5969 + warn-once: "npm:^0.1.0" 5992 5970 peerDependencies: 5993 5971 "@react-navigation/native": ^6.0.0 5994 5972 react: "*" 5995 5973 react-native: "*" 5974 + react-native-gesture-handler: ">= 1.0.0" 5975 + react-native-reanimated: ">= 1.0.0" 5996 5976 react-native-safe-area-context: ">= 3.0.0" 5997 - checksum: 10/caf0321ed2a632aa63473e18d05020228bba81bb39a6e4076fdd17fec2597bcad8cb8d6d7483653ecb465d007e4733c683995ca59144ee908db9a21cb47b13da 5977 + react-native-screens: ">= 3.0.0" 5978 + checksum: 10/970da3f2f78458bbbf30aa2c1a2e071c22a63a4404e2f1f4f6c916b0e33c4faec0693da15a9b52d8f3e0c857008a20f57f310eddb8dd0c1958cfa1aa7fa710ae 5979 + languageName: node 5980 + linkType: hard 5981 + 5982 + "@react-navigation/elements@npm:^1.3.31": 5983 + version: 1.3.31 5984 + resolution: "@react-navigation/elements@npm:1.3.31" 5985 + peerDependencies: 5986 + "@react-navigation/native": ^6.0.0 5987 + react: "*" 5988 + react-native: "*" 5989 + react-native-safe-area-context: ">= 3.0.0" 5990 + checksum: 10/379b3657300f9ab8043979f1ecaea95dce96253903db8d6954468e39dc7cf0710cc08345fa6625071a1505b6442a395e0e20bde39c0b997fd90fea370275fc08 5998 5991 languageName: node 5999 5992 linkType: hard 6000 5993 6001 - "@react-navigation/native-stack@npm:~6.9.12": 6002 - version: 6.9.26 6003 - resolution: "@react-navigation/native-stack@npm:6.9.26" 5994 + "@react-navigation/native-stack@npm:^6.11.0": 5995 + version: 6.11.0 5996 + resolution: "@react-navigation/native-stack@npm:6.11.0" 6004 5997 dependencies: 6005 - "@react-navigation/elements": "npm:^1.3.30" 5998 + "@react-navigation/elements": "npm:^1.3.31" 6006 5999 warn-once: "npm:^0.1.0" 6007 6000 peerDependencies: 6008 6001 "@react-navigation/native": ^6.0.0 ··· 6010 6003 react-native: "*" 6011 6004 react-native-safe-area-context: ">= 3.0.0" 6012 6005 react-native-screens: ">= 3.0.0" 6013 - checksum: 10/75c7bb5647d065f45ab234b9d44f8562641225c02b2c354be9fba6a1b8b53f216e8dd0c3749492fdf598d08bd9569749a1d6ad93fa40f15e9f141b35b0809ef9 6006 + checksum: 10/d27212088dde4ca16c78d8f85187d452d56cc9243506c049d2595b61c10eda44368ab01c53e729777d9900fe5c3a4dfeae2598b1a534508d6a1df8ab35c1a4dc 6014 6007 languageName: node 6015 6008 linkType: hard 6016 6009 6017 - "@react-navigation/native@npm:^6.1.17, @react-navigation/native@npm:~6.1.6": 6018 - version: 6.1.17 6019 - resolution: "@react-navigation/native@npm:6.1.17" 6010 + "@react-navigation/native@npm:^6.1.18": 6011 + version: 6.1.18 6012 + resolution: "@react-navigation/native@npm:6.1.18" 6020 6013 dependencies: 6021 - "@react-navigation/core": "npm:^6.4.16" 6014 + "@react-navigation/core": "npm:^6.4.17" 6022 6015 escape-string-regexp: "npm:^4.0.0" 6023 6016 fast-deep-equal: "npm:^3.1.3" 6024 6017 nanoid: "npm:^3.1.23" 6025 6018 peerDependencies: 6026 6019 react: "*" 6027 6020 react-native: "*" 6028 - checksum: 10/f0b0ef565ddfd5a9bfa2448e0f0d2f18616144877acc6b2330b6b0966ed2c33702c445c553443651acc8488f8af840ffa5ee9353ddfd582e4b3ff26ecb85fbb5 6021 + checksum: 10/1c16813e7d1d796519d0c3a9163de8be6d4af0afa74d9d88ec6729f8c0f533540250f09e39063f4a1eafb9ff71c3f3a9cc9d420ba75aa3eb7f42834f4ba0ee20 6029 6022 languageName: node 6030 6023 linkType: hard 6031 6024 ··· 6046 6039 "@spacingbat3/lss": "npm:^1.0.0" 6047 6040 semver: "npm:^7.3.8" 6048 6041 checksum: 10/8bb04678ddbe9d1d4c0f54e1c668b1ec74f6d159af9255c7b6e4a7d29ec3b098461591ac7de7bb214d1e39c27094e0e4831fac928f0aa234c8e39ed17b400174 6049 - languageName: node 6050 - linkType: hard 6051 - 6052 - "@remix-run/node@npm:^2.7.2": 6053 - version: 2.10.2 6054 - resolution: "@remix-run/node@npm:2.10.2" 6055 - dependencies: 6056 - "@remix-run/server-runtime": "npm:2.10.2" 6057 - "@remix-run/web-fetch": "npm:^4.4.2" 6058 - "@web3-storage/multipart-parser": "npm:^1.0.0" 6059 - cookie-signature: "npm:^1.1.0" 6060 - source-map-support: "npm:^0.5.21" 6061 - stream-slice: "npm:^0.1.2" 6062 - undici: "npm:^6.11.1" 6063 - peerDependencies: 6064 - typescript: ^5.1.0 6065 - peerDependenciesMeta: 6066 - typescript: 6067 - optional: true 6068 - checksum: 10/8a092a6d7e0bfd9a2245882cf916a2097fbb3fa20848b30f038d435d0334f238bf39afc0684be4b48afe4f18d11ac6601d8e7bc54afa8057122444b896473425 6069 - languageName: node 6070 - linkType: hard 6071 - 6072 - "@remix-run/router@npm:1.17.1": 6073 - version: 1.17.1 6074 - resolution: "@remix-run/router@npm:1.17.1" 6075 - checksum: 10/5efc598626cd81688ac26e0abd08204b980831ead8cd2c4b8a27e0c169ee4777fc609fa289c093b93efc3a1e335304698c6961276c2309348444ec7209836c83 6076 - languageName: node 6077 - linkType: hard 6078 - 6079 - "@remix-run/server-runtime@npm:2.10.2": 6080 - version: 2.10.2 6081 - resolution: "@remix-run/server-runtime@npm:2.10.2" 6082 - dependencies: 6083 - "@remix-run/router": "npm:1.17.1" 6084 - "@types/cookie": "npm:^0.6.0" 6085 - "@web3-storage/multipart-parser": "npm:^1.0.0" 6086 - cookie: "npm:^0.6.0" 6087 - set-cookie-parser: "npm:^2.4.8" 6088 - source-map: "npm:^0.7.3" 6089 - turbo-stream: "npm:2.2.0" 6090 - peerDependencies: 6091 - typescript: ^5.1.0 6092 - peerDependenciesMeta: 6093 - typescript: 6094 - optional: true 6095 - checksum: 10/a9906dd479f173676c7b72b7b019c8b13a67c38e80790ea4280b4748587e26d7ffaf1423b61c98888e88c087a139e8023e191ac0e21ae5e3165d990015ddc9ed 6096 - languageName: node 6097 - linkType: hard 6098 - 6099 - "@remix-run/web-blob@npm:^3.1.0": 6100 - version: 3.1.0 6101 - resolution: "@remix-run/web-blob@npm:3.1.0" 6102 - dependencies: 6103 - "@remix-run/web-stream": "npm:^1.1.0" 6104 - web-encoding: "npm:1.1.5" 6105 - checksum: 10/24b95b90e2a5bfb17478c9e88a03c8de8db2cc6cbe744ee845cd8bfc278744f3d0a6f782f63b3361eef59488f544b60172fb98c628f113c936f597b303bc58f3 6106 - languageName: node 6107 - linkType: hard 6108 - 6109 - "@remix-run/web-fetch@npm:^4.4.2": 6110 - version: 4.4.2 6111 - resolution: "@remix-run/web-fetch@npm:4.4.2" 6112 - dependencies: 6113 - "@remix-run/web-blob": "npm:^3.1.0" 6114 - "@remix-run/web-file": "npm:^3.1.0" 6115 - "@remix-run/web-form-data": "npm:^3.1.0" 6116 - "@remix-run/web-stream": "npm:^1.1.0" 6117 - "@web3-storage/multipart-parser": "npm:^1.0.0" 6118 - abort-controller: "npm:^3.0.0" 6119 - data-uri-to-buffer: "npm:^3.0.1" 6120 - mrmime: "npm:^1.0.0" 6121 - checksum: 10/46961dae587d1d9eb1c678113cf8fcbe0e5779b3fdedd8b136c7065b47e967056d1508924992a58887b03ab406c44c838bca5d0f7d08a4bbaab9609178c3fe4b 6122 - languageName: node 6123 - linkType: hard 6124 - 6125 - "@remix-run/web-file@npm:^3.1.0": 6126 - version: 3.1.0 6127 - resolution: "@remix-run/web-file@npm:3.1.0" 6128 - dependencies: 6129 - "@remix-run/web-blob": "npm:^3.1.0" 6130 - checksum: 10/c5ce184fc8e3a8d5736798c9fa784a3416890382be707da927926d173e67227dc60ae2494be680bf0074a00fac5a9a737387ce820349fb2fecdc31be034854a0 6131 - languageName: node 6132 - linkType: hard 6133 - 6134 - "@remix-run/web-form-data@npm:^3.1.0": 6135 - version: 3.1.0 6136 - resolution: "@remix-run/web-form-data@npm:3.1.0" 6137 - dependencies: 6138 - web-encoding: "npm:1.1.5" 6139 - checksum: 10/4eaa98da8f8827d2fd6e676920c6352679b35e0fd11f83d99f8a1ee009e8e51a0c710e8d228cd481ad00a81b3afe53a1db97321f3c6c16e14a3d67858e238a91 6140 - languageName: node 6141 - linkType: hard 6142 - 6143 - "@remix-run/web-stream@npm:^1.1.0": 6144 - version: 1.1.0 6145 - resolution: "@remix-run/web-stream@npm:1.1.0" 6146 - dependencies: 6147 - web-streams-polyfill: "npm:^3.1.1" 6148 - checksum: 10/9904b1539feee3a86d667e9803783dfc78e21b665a4e67edfd795bd1acee753fda88f50abbebf7cffa010539ed5287b4a0d09f55101b80f2c891c15db1066eea 6149 6042 languageName: node 6150 6043 linkType: hard 6151 6044 ··· 8877 8770 languageName: node 8878 8771 linkType: hard 8879 8772 8880 - "@types/cookie@npm:^0.6.0": 8881 - version: 0.6.0 8882 - resolution: "@types/cookie@npm:0.6.0" 8883 - checksum: 10/b883348d5bf88695fbc2c2276b1c49859267a55cae3cf11ea1dccc1b3be15b466e637ce3242109ba27d616c77c6aa4efe521e3d557110b4fdd9bc332a12445c2 8884 - languageName: node 8885 - linkType: hard 8886 - 8887 8773 "@types/debug@npm:^4.1.7": 8888 8774 version: 4.1.12 8889 8775 resolution: "@types/debug@npm:4.1.12" ··· 8947 8833 "@types/minimatch": "npm:*" 8948 8834 "@types/node": "npm:*" 8949 8835 checksum: 10/6ae717fedfdfdad25f3d5a568323926c64f52ef35897bcac8aca8e19bc50c0bd84630bbd063e5d52078b2137d8e7d3c26eabebd1a2f03ff350fff8a91e79fc19 8836 + languageName: node 8837 + linkType: hard 8838 + 8839 + "@types/hammerjs@npm:^2.0.36": 8840 + version: 2.0.46 8841 + resolution: "@types/hammerjs@npm:2.0.46" 8842 + checksum: 10/1b6502d668f45ca49fb488c01f7938d3aa75e989d70c64801c8feded7d659ca1a118f745c1b604d220efe344c93231767d5cc68c05e00e069c14539b6143cfd9 8950 8843 languageName: node 8951 8844 linkType: hard 8952 8845 ··· 9921 9814 languageName: node 9922 9815 linkType: hard 9923 9816 9924 - "@web3-storage/multipart-parser@npm:^1.0.0": 9925 - version: 1.0.0 9926 - resolution: "@web3-storage/multipart-parser@npm:1.0.0" 9927 - checksum: 10/20d7a4330392d83f727586477fc6e709f8fca3b3664365b0d9d3041abe2aeb13f53030a3997a28a39cc74930863ebb625878b8c90789e5c990e4b64d9f22a93d 9928 - languageName: node 9929 - linkType: hard 9930 - 9931 9817 "@webassemblyjs/ast@npm:1.12.1, @webassemblyjs/ast@npm:^1.12.1": 9932 9818 version: 1.12.1 9933 9819 resolution: "@webassemblyjs/ast@npm:1.12.1" ··· 10132 10018 bin: 10133 10019 js-yaml: bin/js-yaml.js 10134 10020 checksum: 10/83642debff31400764e8721ba8f386e0f5444b118c7a6c17dbdcb316b56fefa061ea0587af47de75e04d60059215a703a1ca8bbc479149581cd57d752cb3d4e0 10135 - languageName: node 10136 - linkType: hard 10137 - 10138 - "@zxing/text-encoding@npm:0.9.0": 10139 - version: 0.9.0 10140 - resolution: "@zxing/text-encoding@npm:0.9.0" 10141 - checksum: 10/268e4ef64b8eaa32b990240bdfd1f7b3e2b501a6ed866a565f7c9747f04ac884fbe0537fe12bb05d9241b98fb111270c0fd0023ef0a02d23a6619b4589e98f6b 10142 10021 languageName: node 10143 10022 linkType: hard 10144 10023 ··· 10581 10460 "@rainbow-me/rainbowkit": "npm:2" 10582 10461 "@react-native-firebase/app": "npm:^20.3.0" 10583 10462 "@react-native-firebase/messaging": "npm:^20.3.0" 10584 - "@react-navigation/native": "npm:^6.1.17" 10463 + "@react-navigation/bottom-tabs": "npm:^6.6.1" 10464 + "@react-navigation/drawer": "npm:^6.7.2" 10465 + "@react-navigation/native": "npm:^6.1.18" 10466 + "@react-navigation/native-stack": "npm:^6.11.0" 10585 10467 "@tamagui/babel-plugin": "npm:^1.102.3" 10586 10468 "@tamagui/config": "npm:^1.102.3" 10587 10469 "@tamagui/lucide-icons": "npm:^1.102.3" ··· 10600 10482 expo-font: "npm:~12.0.9" 10601 10483 expo-linking: "npm:~6.3.1" 10602 10484 expo-notifications: "npm:~0.28.10" 10603 - expo-router: "npm:~3.5.18" 10604 10485 expo-splash-screen: "npm:~0.27.5" 10605 10486 expo-status-bar: "npm:^1.12.1" 10606 10487 expo-system-ui: "npm:~3.0.7" ··· 10611 10492 react: "npm:18.3.1" 10612 10493 react-dom: "npm:18.3.1" 10613 10494 react-native: "npm:0.74.3" 10495 + react-native-gesture-handler: "npm:~2.16.1" 10614 10496 react-native-markdown-display: "npm:^7.0.2" 10615 - react-native-reanimated: "npm:~3.14.0" 10616 - react-native-safe-area-context: "npm:4.10.8" 10617 - react-native-screens: "npm:~3.32.0" 10497 + react-native-reanimated: "npm:~3.10.1" 10498 + react-native-safe-area-context: "npm:4.10.5" 10499 + react-native-screens: "npm:3.31.1" 10618 10500 react-native-svg: "npm:15.4.0" 10619 10501 react-native-web: "npm:^0.19.12" 10620 10502 react-native-webview: "npm:13.10.5" ··· 12493 12375 languageName: node 12494 12376 linkType: hard 12495 12377 12496 - "cookie-signature@npm:^1.1.0": 12497 - version: 1.2.1 12498 - resolution: "cookie-signature@npm:1.2.1" 12499 - checksum: 10/b871138a81382173d51dde5c1c56e8b313bc4b9e5f2f67d0d63be50fd43b92a25cb9bd72c2fc2935c0c6cb6cce834e7e2fd12830d7ec289ccac5bdec19dd14eb 12500 - languageName: node 12501 - linkType: hard 12502 - 12503 - "cookie@npm:0.6.0, cookie@npm:^0.6.0": 12378 + "cookie@npm:0.6.0": 12504 12379 version: 0.6.0 12505 12380 resolution: "cookie@npm:0.6.0" 12506 12381 checksum: 10/c1f8f2ea7d443b9331680598b0ae4e6af18a618c37606d1bbdc75bec8361cce09fe93e727059a673f2ba24467131a9fb5a4eec76bb1b149c1b3e1ccb268dc583 ··· 12798 12673 version: 7.0.0 12799 12674 resolution: "dargs@npm:7.0.0" 12800 12675 checksum: 10/b8f1e3cba59c42e1f13a114ad4848c3fc1cf7470f633ee9e9f1043762429bc97d91ae31b826fb135eefde203a3fdb20deb0c0a0222ac29d937b8046085d668d1 12801 - languageName: node 12802 - linkType: hard 12803 - 12804 - "data-uri-to-buffer@npm:^3.0.1": 12805 - version: 3.0.1 12806 - resolution: "data-uri-to-buffer@npm:3.0.1" 12807 - checksum: 10/c59c3009686a78c071806b72f4810856ec28222f0f4e252aa495ec027ed9732298ceea99c50328cf59b151dd34cbc3ad6150bbb43e41fc56fa19f48c99e9fc30 12808 12676 languageName: node 12809 12677 linkType: hard 12810 12678 ··· 14622 14490 languageName: node 14623 14491 linkType: hard 14624 14492 14625 - "expo-router@npm:~3.5.18": 14626 - version: 3.5.18 14627 - resolution: "expo-router@npm:3.5.18" 14628 - dependencies: 14629 - "@expo/metro-runtime": "npm:3.2.1" 14630 - "@expo/server": "npm:^0.4.0" 14631 - "@radix-ui/react-slot": "npm:1.0.1" 14632 - "@react-navigation/bottom-tabs": "npm:~6.5.7" 14633 - "@react-navigation/native": "npm:~6.1.6" 14634 - "@react-navigation/native-stack": "npm:~6.9.12" 14635 - expo-splash-screen: "npm:0.27.5" 14636 - react-native-helmet-async: "npm:2.0.4" 14637 - schema-utils: "npm:^4.0.1" 14638 - peerDependencies: 14639 - "@react-navigation/drawer": ^6.5.8 14640 - expo: "*" 14641 - expo-constants: "*" 14642 - expo-linking: "*" 14643 - expo-status-bar: "*" 14644 - react-native-reanimated: "*" 14645 - react-native-safe-area-context: "*" 14646 - react-native-screens: "*" 14647 - peerDependenciesMeta: 14648 - "@react-navigation/drawer": 14649 - optional: true 14650 - "@testing-library/jest-native": 14651 - optional: true 14652 - react-native-reanimated: 14653 - optional: true 14654 - checksum: 10/94aa615155ffb2efd706ae54c0c6398836b15e8a583762d132e0ab2286dccaacfd87331df00c458be1f0812af4e95559e5bae2787ead43048f856d93f44432c7 14655 - languageName: node 14656 - linkType: hard 14657 - 14658 - "expo-splash-screen@npm:0.27.5, expo-splash-screen@npm:~0.27.5": 14493 + "expo-splash-screen@npm:~0.27.5": 14659 14494 version: 0.27.5 14660 14495 resolution: "expo-splash-screen@npm:0.27.5" 14661 14496 dependencies: ··· 16484 16319 minimalistic-assert: "npm:^1.0.0" 16485 16320 minimalistic-crypto-utils: "npm:^1.0.1" 16486 16321 checksum: 10/0298a1445b8029a69b713d918ecaa84a1d9f614f5857e0c6e1ca517abfa1357216987b2ee08cc6cc73ba82a6c6ddf2ff11b9717a653530ef03be599d4699b836 16322 + languageName: node 16323 + linkType: hard 16324 + 16325 + "hoist-non-react-statics@npm:^3.3.0": 16326 + version: 3.3.2 16327 + resolution: "hoist-non-react-statics@npm:3.3.2" 16328 + dependencies: 16329 + react-is: "npm:^16.7.0" 16330 + checksum: 10/1acbe85f33e5a39f90c822ad4d28b24daeb60f71c545279431dc98c312cd28a54f8d64788e477fe21dc502b0e3cf58589ebe5c1ad22af27245370391c2d24ea6 16487 16331 languageName: node 16488 16332 linkType: hard 16489 16333 ··· 20079 19923 languageName: node 20080 19924 linkType: hard 20081 19925 20082 - "mrmime@npm:^1.0.0": 20083 - version: 1.0.1 20084 - resolution: "mrmime@npm:1.0.1" 20085 - checksum: 10/a157e833ffe76648ab2107319deeff024b80b136ec66c60fae9d339009a1bb72c57ec1feecfd6a905dfd3df29e2299e850bff84b69cad790cc9bd9ab075834d1 20086 - languageName: node 20087 - linkType: hard 20088 - 20089 19926 "ms@npm:2.0.0": 20090 19927 version: 2.0.0 20091 19928 resolution: "ms@npm:2.0.0" ··· 22339 22176 peerDependencies: 22340 22177 react: ^18.3.1 22341 22178 checksum: 10/3f4b73a3aa083091173b29812b10394dd06f4ac06aff410b74702cfb3aa29d7b0ced208aab92d5272919b612e5cda21aeb1d54191848cf6e46e9e354f3541f81 22342 - languageName: node 22343 - linkType: hard 22344 - 22345 - "react-fast-compare@npm:^3.2.2": 22346 - version: 3.2.2 22347 - resolution: "react-fast-compare@npm:3.2.2" 22348 - checksum: 10/a6826180ba75cefba1c8d3ac539735f9b627ca05d3d307fe155487f5d0228d376dac6c9708d04a283a7b9f9aee599b637446635b79c8c8753d0b4eece56c125c 22349 22179 languageName: node 22350 22180 linkType: hard 22351 22181 ··· 22365 22195 languageName: node 22366 22196 linkType: hard 22367 22197 22368 - "react-is@npm:^16.13.0, react-is@npm:^16.13.1": 22198 + "react-is@npm:^16.13.0, react-is@npm:^16.13.1, react-is@npm:^16.7.0": 22369 22199 version: 16.13.1 22370 22200 resolution: "react-is@npm:16.13.1" 22371 22201 checksum: 10/5aa564a1cde7d391ac980bedee21202fc90bdea3b399952117f54fb71a932af1e5902020144fb354b4690b2414a0c7aafe798eb617b76a3d441d956db7726fdf ··· 22411 22241 languageName: node 22412 22242 linkType: hard 22413 22243 22414 - "react-native-helmet-async@npm:2.0.4": 22415 - version: 2.0.4 22416 - resolution: "react-native-helmet-async@npm:2.0.4" 22244 + "react-native-gesture-handler@npm:~2.16.1": 22245 + version: 2.16.2 22246 + resolution: "react-native-gesture-handler@npm:2.16.2" 22417 22247 dependencies: 22248 + "@egjs/hammerjs": "npm:^2.0.17" 22249 + hoist-non-react-statics: "npm:^3.3.0" 22418 22250 invariant: "npm:^2.2.4" 22419 - react-fast-compare: "npm:^3.2.2" 22420 - shallowequal: "npm:^1.1.0" 22251 + lodash: "npm:^4.17.21" 22252 + prop-types: "npm:^15.7.2" 22421 22253 peerDependencies: 22422 - react: ^16.6.0 || ^17.0.0 || ^18.0.0 22423 - checksum: 10/217bd0eaa61d426a512634369c70c44ce8b92127ec626dc40c65b72b1be1534ed3ed00ba2dd1a9ad77d1716ce8fd1e6db3f7209534303a652d9932a962d2c830 22254 + react: "*" 22255 + react-native: "*" 22256 + checksum: 10/227799f5e16f9725d81db524fc06c1fbef226835a5ee989642d132748832f7543ebc750d4309e49bcaa0fb6d1e7dd6b74028785e4b755978ab7f66f05b414c18 22424 22257 languageName: node 22425 22258 linkType: hard 22426 22259 ··· 22439 22272 languageName: node 22440 22273 linkType: hard 22441 22274 22442 - "react-native-reanimated@npm:~3.14.0": 22443 - version: 3.14.0 22444 - resolution: "react-native-reanimated@npm:3.14.0" 22275 + "react-native-reanimated@npm:~3.10.1": 22276 + version: 3.10.1 22277 + resolution: "react-native-reanimated@npm:3.10.1" 22445 22278 dependencies: 22446 22279 "@babel/plugin-transform-arrow-functions": "npm:^7.0.0-0" 22447 22280 "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.0.0-0" ··· 22455 22288 "@babel/core": ^7.0.0-0 22456 22289 react: "*" 22457 22290 react-native: "*" 22458 - checksum: 10/b780949f52bb0d66521c973173d01776febbedffa4ee86309f31b2cf6b1cae2d0a0b7e6a177c2dbc5f6d92ba3912b6b8bde9b3b42dd39fc0ef792acbd909096b 22291 + checksum: 10/bb2a0802fd619999dd5d1dd9734875fc14699de29d3537003997e85040dd3a0b430716192388c71544d600fc7abf243af1cfc24ce2513c282b046e8bb3430125 22459 22292 languageName: node 22460 22293 linkType: hard 22461 22294 22462 - "react-native-safe-area-context@npm:4.10.8": 22463 - version: 4.10.8 22464 - resolution: "react-native-safe-area-context@npm:4.10.8" 22295 + "react-native-safe-area-context@npm:4.10.5": 22296 + version: 4.10.5 22297 + resolution: "react-native-safe-area-context@npm:4.10.5" 22465 22298 peerDependencies: 22466 22299 react: "*" 22467 22300 react-native: "*" 22468 - checksum: 10/3d8151b09ea6a6558e9ece7c3ef62904f6e19931e281ac4dbe900e3f63469bd834d6424c36d4e3a550de290cca5ac591e5f376de73f04a5a209edcb95abff0f9 22301 + checksum: 10/0bc7b4cd442e7a5deb0470f3b915ec2cf6b54337ea527fc7f5bde4f4dd062d420cb2b40fb886df96853f01951cfcb69a35fb4f6f47561cef5e195d9d68c54c3c 22469 22302 languageName: node 22470 22303 linkType: hard 22471 22304 22472 - "react-native-screens@npm:~3.32.0": 22473 - version: 3.32.0 22474 - resolution: "react-native-screens@npm:3.32.0" 22305 + "react-native-screens@npm:3.31.1": 22306 + version: 3.31.1 22307 + resolution: "react-native-screens@npm:3.31.1" 22475 22308 dependencies: 22476 22309 react-freeze: "npm:^1.0.0" 22477 22310 warn-once: "npm:^0.1.0" 22478 22311 peerDependencies: 22479 22312 react: "*" 22480 22313 react-native: "*" 22481 - checksum: 10/22f27b2082e0c8d70165c8a26dcf55b4da2a18166d4b9a0b434f558edea57d71458800d5086940fe7cabe8b7e28c8614cf227f35152171d08583f4ef6f6f3a35 22314 + checksum: 10/54a44fc5a34848195a8e004b9ee2269e8a3560c5fa7922d80eaa1fc6283990d6ef77b3d3ec6004d7df2ad95279d486faf25bb68a82a0e958493891adb7dbac9c 22482 22315 languageName: node 22483 22316 linkType: hard 22484 22317 ··· 23471 23304 languageName: node 23472 23305 linkType: hard 23473 23306 23474 - "schema-utils@npm:^4.0.0, schema-utils@npm:^4.0.1": 23307 + "schema-utils@npm:^4.0.0": 23475 23308 version: 4.2.0 23476 23309 resolution: "schema-utils@npm:4.2.0" 23477 23310 dependencies: ··· 23668 23501 languageName: node 23669 23502 linkType: hard 23670 23503 23671 - "set-cookie-parser@npm:^2.4.8": 23672 - version: 2.6.0 23673 - resolution: "set-cookie-parser@npm:2.6.0" 23674 - checksum: 10/8d451ebadb760989f93b634942c79de3c925ca7a986d133d08a80c40b5ae713ce12e354f0d5245c49f288c52daa7bd6554d5dc52f8a4eecaaf5e192881cf2b1f 23675 - languageName: node 23676 - linkType: hard 23677 - 23678 23504 "set-function-length@npm:^1.2.1": 23679 23505 version: 1.2.2 23680 23506 resolution: "set-function-length@npm:1.2.2" ··· 23771 23597 languageName: node 23772 23598 linkType: hard 23773 23599 23774 - "shallowequal@npm:^1.1.0": 23775 - version: 1.1.0 23776 - resolution: "shallowequal@npm:1.1.0" 23777 - checksum: 10/f4c1de0837f106d2dbbfd5d0720a5d059d1c66b42b580965c8f06bb1db684be8783538b684092648c981294bf817869f743a066538771dbecb293df78f765e00 23778 - languageName: node 23779 - linkType: hard 23780 - 23781 23600 "shebang-command@npm:^1.2.0": 23782 23601 version: 1.2.0 23783 23602 resolution: "shebang-command@npm:1.2.0" ··· 24293 24112 version: 1.0.3 24294 24113 resolution: "stream-shift@npm:1.0.3" 24295 24114 checksum: 10/a24c0a3f66a8f9024bd1d579a533a53be283b4475d4e6b4b3211b964031447bdf6532dd1f3c2b0ad66752554391b7c62bd7ca4559193381f766534e723d50242 24296 - languageName: node 24297 - linkType: hard 24298 - 24299 - "stream-slice@npm:^0.1.2": 24300 - version: 0.1.2 24301 - resolution: "stream-slice@npm:0.1.2" 24302 - checksum: 10/6fa948ea58523f11f72e796f99579ff2bbecdff080d63b25762b0c0d282ac9a2d98af0f6e84dcc8d24c6284b2f7ce92ce0f9a1c8f77c91ac273954754e53c781 24303 24115 languageName: node 24304 24116 linkType: hard 24305 24117 ··· 25226 25038 languageName: node 25227 25039 linkType: hard 25228 25040 25229 - "turbo-stream@npm:2.2.0": 25230 - version: 2.2.0 25231 - resolution: "turbo-stream@npm:2.2.0" 25232 - checksum: 10/1a61be95585d6c901c7f626497765e28c38332f6562f4206e8a295dca87297d044f2413f369764365a6cbb13aae3c21fee2aace4525ff32c1e787e418916914e 25233 - languageName: node 25234 - linkType: hard 25235 - 25236 25041 "type-check@npm:^0.4.0, type-check@npm:~0.4.0": 25237 25042 version: 0.4.0 25238 25043 resolution: "type-check@npm:0.4.0" ··· 25535 25340 languageName: node 25536 25341 linkType: hard 25537 25342 25538 - "undici@npm:^6.11.1": 25539 - version: 6.19.2 25540 - resolution: "undici@npm:6.19.2" 25541 - checksum: 10/f4895c0c1e2fcde18076ac98965c231875811b8e14ceab1a7b19e940ed1a2fce69e0864e1186bfdc9347dffe0f39a00daf6dd5e7c0169b2b33de59a859dc2f1d 25542 - languageName: node 25543 - linkType: hard 25544 - 25545 25343 "unenv@npm:^1.9.0": 25546 25344 version: 1.10.0 25547 25345 resolution: "unenv@npm:1.10.0" ··· 25831 25629 languageName: node 25832 25630 linkType: hard 25833 25631 25834 - "use-latest-callback@npm:^0.1.9": 25835 - version: 0.1.11 25836 - resolution: "use-latest-callback@npm:0.1.11" 25632 + "use-latest-callback@npm:^0.2.1": 25633 + version: 0.2.1 25634 + resolution: "use-latest-callback@npm:0.2.1" 25837 25635 peerDependencies: 25838 25636 react: ">=16.8" 25839 - checksum: 10/cc6df404a4ed3a39d0eb014a815c2e568a6abbe8c5ff09f9205a08bf68e86201826ed633865a6056ca0fd9581e0e7e70f18ca26c592a9eaccea5d244cf283b1f 25637 + checksum: 10/da5718eda625738cc7dac8fb502d0f8f2039435eb71203565a72c32e0f5769e7b8ddac074e650066636e7f4b29b45524f751cb18a2b430856d98879bbb10d274 25840 25638 languageName: node 25841 25639 linkType: hard 25842 25640 ··· 25892 25690 languageName: node 25893 25691 linkType: hard 25894 25692 25895 - "util@npm:^0.12.3, util@npm:^0.12.4, util@npm:^0.12.5": 25693 + "util@npm:^0.12.4, util@npm:^0.12.5": 25896 25694 version: 0.12.5 25897 25695 resolution: "util@npm:0.12.5" 25898 25696 dependencies: ··· 26123 25921 dependencies: 26124 25922 defaults: "npm:^1.0.3" 26125 25923 checksum: 10/182ebac8ca0b96845fae6ef44afd4619df6987fe5cf552fdee8396d3daa1fb9b8ec5c6c69855acb7b3c1231571393bd1f0a4cdc4028d421575348f64bb0a8817 26126 - languageName: node 26127 - linkType: hard 26128 - 26129 - "web-encoding@npm:1.1.5": 26130 - version: 1.1.5 26131 - resolution: "web-encoding@npm:1.1.5" 26132 - dependencies: 26133 - "@zxing/text-encoding": "npm:0.9.0" 26134 - util: "npm:^0.12.3" 26135 - dependenciesMeta: 26136 - "@zxing/text-encoding": 26137 - optional: true 26138 - checksum: 10/243518cfa8388ac05eeb4041bd330d38c599476ff9a93239b386d1ba2af130089a2fcefb0cf65b385f989105ff460ae69dca7e42236f4d98dc776b04e558cdb5 26139 - languageName: node 26140 - linkType: hard 26141 - 26142 - "web-streams-polyfill@npm:^3.1.1": 26143 - version: 3.3.3 26144 - resolution: "web-streams-polyfill@npm:3.3.3" 26145 - checksum: 10/8e7e13501b3834094a50abe7c0b6456155a55d7571312b89570012ef47ec2a46d766934768c50aabad10a9c30dd764a407623e8bfcc74fcb58495c29130edea9 26146 25924 languageName: node 26147 25925 linkType: hard 26148 25926