diff --git a/js/app/components/mobile/player.tsx b/js/app/components/mobile/player.tsx index 5ccb1a0e4..cd1beab13 100644 --- a/js/app/components/mobile/player.tsx +++ b/js/app/components/mobile/player.tsx @@ -511,7 +511,17 @@ export function PlayerInner( ) )} - {!props.showUnavailable && } + {props.mode !== "vod" && !props.showUnavailable && ( + + )} + {props.mode === "vod" && ( + + + + + )} x.url); + if (!src) return BBB_HLS_URL; + if (src.startsWith("http://") || src.startsWith("https://")) return src; + return `${serverUrl}/api/playback/${src}/hls/index.m3u8`; +} + +export default function VideoScreen({ + route, +}: { + route?: { params?: { src?: string } }; +}) { + const src = route?.params?.src; + const url = usePlaybackUrl(src); + + return ( + + + + + {src && ( + + + {src} + + + )} + + ); +} diff --git a/js/components/src/components/mobile-player/player.tsx b/js/components/src/components/mobile-player/player.tsx index edd6372a4..96ac673f5 100644 --- a/js/components/src/components/mobile-player/player.tsx +++ b/js/components/src/components/mobile-player/player.tsx @@ -28,6 +28,7 @@ export function Player( const setReportingURL = usePlayerStore((x) => x.setReportingURL); const setEmbedded = usePlayerStore((x) => x.setEmbedded); + const setMode = usePlayerStore((x) => x.setMode); const reportModalOpen = usePlayerStore((x) => x.reportModalOpen); const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen); @@ -58,6 +59,10 @@ export function Player( setEmbedded(props.embedded ?? false); }, [props.embedded]); + useEffect(() => { + setMode(props.mode ?? "live"); + }, [props.mode]); + // Will call back every few seconds to send health updates usePlayerStatus(); diff --git a/js/components/src/components/mobile-player/props.tsx b/js/components/src/components/mobile-player/props.tsx index 7b4c5e430..6b00e9353 100644 --- a/js/components/src/components/mobile-player/props.tsx +++ b/js/components/src/components/mobile-player/props.tsx @@ -1,8 +1,10 @@ +import { PlayerMode } from "../../player-store/player-state"; + export type PlayerProps = { name: string; playerId?: string; src: string; - mode?: "live" | "vod"; + mode?: PlayerMode; muted: boolean; telemetry: boolean; fullscreen: boolean; diff --git a/js/components/src/components/mobile-player/ui/index.ts b/js/components/src/components/mobile-player/ui/index.ts index e30e98da2..ac94bddfc 100644 --- a/js/components/src/components/mobile-player/ui/index.ts +++ b/js/components/src/components/mobile-player/ui/index.ts @@ -3,9 +3,11 @@ export * from "./autoplay-button"; export * from "./countdown"; export * from "./input"; export * from "./metrics"; +export * from "./seek-bar"; export * from "./streamer-context-menu"; export * from "./streamer-loading-overlay"; export * from "./viewer-context-menu"; export * from "./viewer-count"; export * from "./viewer-loading-overlay"; export * from "./viewers"; +export * from "./vod-controls"; diff --git a/js/components/src/components/mobile-player/ui/seek-bar.tsx b/js/components/src/components/mobile-player/ui/seek-bar.tsx new file mode 100644 index 000000000..eb4f8801a --- /dev/null +++ b/js/components/src/components/mobile-player/ui/seek-bar.tsx @@ -0,0 +1,192 @@ +import { useCallback, useRef, useState } from "react"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withSpring, +} from "react-native-reanimated"; +import { usePlayerStore } from "../../../player-store"; +import { Slider } from "../../ui"; +import { Text, View } from "../../ui/index"; + +function formatTime(seconds: number): string { + const s = Math.floor(seconds); + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const sec = s % 60; + const pad = (n: number) => String(n).padStart(2, "0"); + if (h > 0) return `${h}:${pad(m)}:${pad(sec)}`; + return `${m}:${pad(sec)}`; +} + +export function SeekBar() { + const mode = usePlayerStore((x) => x.mode); + const playTime = usePlayerStore((x) => x.playTime); + const duration = usePlayerStore((x) => x.duration); + const bufferedEnd = usePlayerStore((x) => x.bufferedEnd); + const seekTo = usePlayerStore((x) => x.seekTo); + + const seekingRef = useRef(false); + const [seekValue, setSeekValue] = useState(0); + + const thumbHovered = useSharedValue(0); + const thumbAnimStyle = useAnimatedStyle(() => ({ + boxShadow: `0 2px 10px rgba(0, 0, 0, ${withSpring(thumbHovered.value === 1 ? 1 : 0.18)})`, + transform: [ + { scaleY: withSpring(thumbHovered.value === 1 ? 1 : 0.18) }, + { + scaleX: withSpring(thumbHovered.value === 1 ? 1 : 0.35), + }, + ], + })); + + const handleValueChange = useCallback((vals: number[]) => { + seekingRef.current = true; + setSeekValue(vals[0]); + }, []); + + const handlePointerUp = useCallback(() => { + if (seekingRef.current) { + seekTo(seekValue); + seekingRef.current = false; + } + }, [seekValue, seekTo]); + + if (mode !== "vod" || duration <= 0) return null; + + const displayTime = seekingRef.current ? seekValue : playTime; + + return ( + + + + + { + thumbHovered.value = 1; + }} + // @ts-ignore — web-only pointer events + onPointerLeave={() => { + thumbHovered.value = 0; + }} + /> + {/* full track background */} + + {/* buffered range */} + + + + + + + + + + + + + {formatTime(displayTime)} + + + {formatTime(duration)} + + + + ); +} diff --git a/js/components/src/components/mobile-player/ui/vod-controls.tsx b/js/components/src/components/mobile-player/ui/vod-controls.tsx new file mode 100644 index 000000000..fc448d867 --- /dev/null +++ b/js/components/src/components/mobile-player/ui/vod-controls.tsx @@ -0,0 +1,102 @@ +import { Gauge, Pause, Play } from "lucide-react-native"; +import { Pressable } from "react-native"; +import { useLivestreamStore } from "../../../livestream-store"; +import { PlayerStatus, usePlayerStore } from "../../../player-store"; + +import { + DropdownMenu, + DropdownMenuGroup, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, + ResponsiveDropdownMenuContent, + Text, + useTheme, + View, +} from "../../ui"; + +export function VodControls() { + const mode = usePlayerStore((x) => x.mode); + const status = usePlayerStore((x) => x.status); + const videoRef = usePlayerStore((x) => x.videoRef); + const quality = usePlayerStore((x) => x.selectedRendition); + const setQuality = usePlayerStore((x) => x.setSelectedRendition); + const liveRenditions = useLivestreamStore((x) => x.renditions); + const vodLevels = usePlayerStore((x) => x.vodLevels); + const renditions = mode === "vod" ? vodLevels : liveRenditions; + const th = useTheme(); + + if (mode !== "vod") return null; + + const isPlaying = status === PlayerStatus.PLAYING; + + const togglePlayPause = () => { + if ( + videoRef && + typeof videoRef === "object" && + "current" in videoRef && + videoRef.current + ) { + if (videoRef.current.paused) { + videoRef.current.play(); + } else { + videoRef.current.pause(); + } + } + }; + + return ( + + + {isPlaying ? ( + + ) : ( + + )} + + + + + {renditions.length > 0 && ( + + + + + + + + + {mode === "vod" ? "Auto" : "Source"} + + {renditions.map((r) => ( + + {r.name === "audio" ? "Audio Only" : r.name} + + ))} + + + + + )} + + ); +} diff --git a/js/components/src/components/mobile-player/video-retry.tsx b/js/components/src/components/mobile-player/video-retry.tsx index 2f6eed99b..a7e461f0c 100644 --- a/js/components/src/components/mobile-player/video-retry.tsx +++ b/js/components/src/components/mobile-player/video-retry.tsx @@ -13,7 +13,10 @@ export default function VideoRetry(props: { children: React.ReactNode }) { setLastChange(Date.now()); }, [status]); + const mode = usePlayerStore((x) => x.mode); + useEffect(() => { + if (mode === "vod") return; if (!playing) { const handle = setInterval(() => { setTick(Date.now()); @@ -41,7 +44,7 @@ export default function VideoRetry(props: { children: React.ReactNode }) { retryTimeoutRef.current = null; } }; - }, [!playing]); + }, [!playing, mode]); useEffect(() => { if (stalledFor5sec) { diff --git a/js/components/src/components/mobile-player/video.tsx b/js/components/src/components/mobile-player/video.tsx index c91e2ab82..1deffafc0 100644 --- a/js/components/src/components/mobile-player/video.tsx +++ b/js/components/src/components/mobile-player/video.tsx @@ -149,6 +149,9 @@ const VideoElement = forwardRef< const setStatus = usePlayerStore((x) => x.setStatus); const setUserInteraction = usePlayerStore((x) => x.setUserInteraction); const setVideoRef = usePlayerStore((x) => x.setVideoRef); + const setPlayTime = usePlayerStore((x) => x.setPlayTime); + const setDuration = usePlayerStore((x) => x.setDuration); + const setBufferedEnd = usePlayerStore((x) => x.setBufferedEnd); const event = (evType) => (e) => { console.log(evType); @@ -297,6 +300,19 @@ const VideoElement = forwardRef< onSeeking={event("seeking")} onStalled={event("stalled")} onSuspend={event("suspend")} + onTimeUpdate={(e) => { + setPlayTime((e.target as HTMLVideoElement).currentTime); + }} + onDurationChange={(e) => { + const d = (e.target as HTMLVideoElement).duration; + if (isFinite(d)) setDuration(d); + }} + onProgress={(e) => { + const el = e.target as HTMLVideoElement; + if (el.buffered.length > 0) { + setBufferedEnd(el.buffered.end(el.buffered.length - 1)); + } + }} onVolumeChange={event("volumechange")} onWaiting={event("waiting")} style={{ @@ -323,6 +339,10 @@ export function ProgressiveWebMPlayer(props: VideoProps) { export function HLSPlayer(props: VideoProps) { const localRef = useRef(null); + const hlsRef = useRef(null); + const setVodLevels = usePlayerStore((x) => x.setVodLevels); + const selectedRendition = usePlayerStore((x) => x.selectedRendition); + const mode = usePlayerStore((x) => x.mode); // other players set some status on start, HLS doesn't, so // do this to make sure we we reset off of "error" state @@ -337,6 +357,7 @@ export function HLSPlayer(props: VideoProps) { } if (Hls.isSupported()) { var hls = new Hls({ maxAudioFramesDrift: 20 }); + hlsRef.current = hls; hls.loadSource(props.url); try { hls.attachMedia(localRef.current); @@ -350,9 +371,21 @@ export function HLSPlayer(props: VideoProps) { return; } localRef.current.play(); + if (mode === "vod" && hls.levels.length > 1) { + setVodLevels( + hls.levels.map((l) => ({ + name: + l.height > 0 + ? `${l.height}p` + : `${Math.round(l.bitrate / 1000)}k`, + })), + ); + } }); return () => { hls.stopLoad(); + hlsRef.current = null; + setVodLevels([]); }; } else if (localRef.current.canPlayType("application/vnd.apple.mpegurl")) { localRef.current.src = props.url; @@ -365,6 +398,21 @@ export function HLSPlayer(props: VideoProps) { } }, [props.url]); + useEffect(() => { + const hls = hlsRef.current; + if (!hls || mode !== "vod") return; + if (selectedRendition === "source" || selectedRendition === "auto") { + hls.currentLevel = -1; + return; + } + const idx = hls.levels.findIndex((l) => { + const name = + l.height > 0 ? `${l.height}p` : `${Math.round(l.bitrate / 1000)}k`; + return name === selectedRendition; + }); + if (idx !== -1) hls.currentLevel = idx; + }, [selectedRendition, mode]); + return ; } diff --git a/js/components/src/player-store/player-state.tsx b/js/components/src/player-store/player-state.tsx index fa48bc219..e01557b50 100644 --- a/js/components/src/player-store/player-state.tsx +++ b/js/components/src/player-store/player-state.tsx @@ -8,6 +8,8 @@ export enum PlayerProtocol { PROGRESSIVE_WEBM = "progressive-webm", } +export type PlayerMode = "live" | "vod"; + export enum PlayerStatus { START = "start", PLAYING = "playing", @@ -27,8 +29,18 @@ export enum IngestMediaSource { export interface PlayerState { id: string; + mode: PlayerMode; + setMode: (mode: PlayerMode) => void; + duration: number; + setDuration: (duration: number) => void; + bufferedEnd: number; + setBufferedEnd: (bufferedEnd: number) => void; selectedRendition: string; setSelectedRendition: (rendition: string) => void; + + /** Quality levels parsed from HLS manifest (VOD only) */ + vodLevels: Array<{ name: string }>; + setVodLevels: (levels: Array<{ name: string }>) => void; protocol: PlayerProtocol; setProtocol: (protocol: PlayerProtocol) => void; @@ -84,7 +96,9 @@ export interface PlayerState { /** Function to set the current playback time */ setPlayTime: (playTime: number) => void; - /** Flag indicating if player is in offline state */ + /** Seek the video element to a specific time */ + seekTo: (time: number) => void; + /** Reference to the video element for direct manipulation (used for PiP) */ videoRef: | React.MutableRefObject diff --git a/js/components/src/player-store/player-store.tsx b/js/components/src/player-store/player-store.tsx index 9bf244deb..46fd58fa7 100644 --- a/js/components/src/player-store/player-store.tsx +++ b/js/components/src/player-store/player-store.tsx @@ -21,6 +21,15 @@ export type PlayerStore = StoreApi; export const makePlayerStore = (id?: string): StoreApi => { const store = createStore()((set) => ({ id: id || Math.random().toString(36).slice(8), + mode: "live", + setMode: (mode) => set(() => ({ mode })), + duration: 0, + setDuration: (duration) => set(() => ({ duration })), + bufferedEnd: 0, + setBufferedEnd: (bufferedEnd) => set(() => ({ bufferedEnd })), + vodLevels: [], + setVodLevels: (vodLevels) => set(() => ({ vodLevels })), + selectedRendition: "source", setSelectedRendition: (rendition: string) => set((state) => { @@ -91,6 +100,14 @@ export const makePlayerStore = (id?: string): StoreApi => { playTime: 0, setPlayTime: (playTime: number) => set(() => ({ playTime })), + seekTo: (time: number) => + set((state) => { + const ref = state.videoRef; + if (ref && typeof ref === "object" && "current" in ref && ref.current) { + ref.current.currentTime = time; + } + return { playTime: time }; + }), videoRef: undefined, setVideoRef: ( -- 2.51.2 From 4c109593773569857359ddcbc58bcae0c27922cf Mon Sep 17 00:00:00 2001 From: "Natalie B." <22222885+espeon@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:05:30 -0500 Subject: [PATCH 02/47] track VOD renditions, refine mobile UI --- .../mobile/desktop-ui/bottom-controls.tsx | 111 +++-- js/app/components/mobile/player.tsx | 97 +--- js/app/components/mobile/ui.tsx | 15 + .../components/mobile-player/ui/seek-bar.tsx | 31 +- .../mobile-player/ui/viewer-context-menu.tsx | 455 +++++++----------- .../mobile-player/ui/vod-controls.tsx | 17 +- .../mobile-player/video-async.native.tsx | 71 ++- .../src/components/mobile-player/video.tsx | 11 + .../src/player-store/player-state.tsx | 7 + .../src/player-store/player-store.tsx | 17 + 10 files changed, 390 insertions(+), 442 deletions(-) diff --git a/js/app/components/mobile/desktop-ui/bottom-controls.tsx b/js/app/components/mobile/desktop-ui/bottom-controls.tsx index 9351659ab..1ac29fa35 100644 --- a/js/app/components/mobile/desktop-ui/bottom-controls.tsx +++ b/js/app/components/mobile/desktop-ui/bottom-controls.tsx @@ -1,6 +1,8 @@ import { Button, + PlayerStatus, PlayerUI, + Text, View, useDanmuEnabled, useDanmuUnlocked, @@ -14,7 +16,9 @@ import { ChevronRight, Fullscreen, Minimize, + Pause, PictureInPicture2, + Play, } from "lucide-react-native"; import { Platform, Pressable } from "react-native"; import { useIsSidebarCollapsed } from "store/hooks"; @@ -116,6 +120,16 @@ function CollapseChatButton({ ); } +function formatTime(seconds: number): string { + const s = Math.floor(seconds); + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const sec = s % 60; + const pad = (n: number) => String(n).padStart(2, "0"); + if (h > 0) return `${h}:${pad(m)}:${pad(sec)}`; + return `${m}:${pad(sec)}`; +} + export function BottomControlBar({ ingest, pipSupported, @@ -125,41 +139,76 @@ export function BottomControlBar({ showChat, setShowChat, }: BottomControlBarProps) { - let { theme } = useTheme(); - const fullscreen = usePlayerStore((state) => state.fullscreen); - const setFullscreen = usePlayerStore((state) => state.setFullscreen); - const danmuUnlocked = useDanmuUnlocked(); - const danmuEnabled = useDanmuEnabled(); - const setDanmuEnabled = useSetDanmuEnabled(); + const th = useTheme(); const sidebarCollapsed = useIsSidebarCollapsed(); + const playbackMode = usePlayerStore((x) => x.mode); + const togglePlayPause = usePlayerStore((x) => x.togglePlayPause); + + const playTime = usePlayerStore((x) => x.playTime); + const duration = usePlayerStore((x) => x.duration); + + const status = usePlayerStore((x) => x.status); + const PlayPause = status === PlayerStatus.PLAYING ? Pause : Play; return ( - - - - + + + + + {playbackMode === "vod" && ( + + + + )} + + {playbackMode === "vod" && ( + + + {formatTime(playTime)} + + / + + {formatTime(duration)} + + + )} + - - {pipSupported && ( - - )} - - {ingest === null && ( - - )} - - {setShowChat && ( - - )} + + {pipSupported && ( + + )} + + {ingest === null && ( + + )} + + {setShowChat && ( + + )} + ); diff --git a/js/app/components/mobile/player.tsx b/js/app/components/mobile/player.tsx index cd1beab13..7622679b8 100644 --- a/js/app/components/mobile/player.tsx +++ b/js/app/components/mobile/player.tsx @@ -31,7 +31,6 @@ import { } from "react-native"; import Reanimated, { useAnimatedStyle, - useDerivedValue, useSharedValue, withTiming, } from "react-native-reanimated"; @@ -81,14 +80,8 @@ function PlayerWithProvider( const { top: safeTop } = useSafeAreaInsets(); const segDims = useSegmentDimensions(); const isPortrait = screenHeight > screenWidth; - const portraitFadeOpacity = useSharedValue(1); - const portraitVideoTranslateY = useDerivedValue( - () => (portraitFadeOpacity.value - 1) * 20, - ); - const portraitVideoStyle = useAnimatedStyle(() => ({ - transform: [{ translateY: (portraitFadeOpacity.value - 1) * 20 }], - })); const isPortraitLandscapeCase = + Platform.OS !== "web" && isPortrait && segDims.width > segDims.height && !shouldShowChatSidePanel && @@ -251,25 +244,19 @@ function PlayerWithProvider( flex: 1, width: "100%", height: "100%", - paddingTop: - isPortraitLandscapeCase && Platform.OS !== "web" - ? 54 - : undefined, + paddingTop: isPortraitLandscapeCase ? 54 : undefined, }, ]} > - - + {isPortraitLandscapeCase ? ( <> - + {!showUnavailable && ( - + )} ) : shouldShowChatSidePanel ? ( ) : ( !showUnavailable && @@ -309,53 +287,6 @@ function PlayerWithProvider( ); } -function MobileUiOverlay({ - showChat, - setShowChat, - isPortraitLandscapeCase, - mode, -}: { - showChat: boolean; - setShowChat: (show: boolean) => void; - isPortraitLandscapeCase: boolean; - mode?: string; -}) { - const { width, height } = usePlayerDimensions(); - const aspectRatio = width > 0 && height > 0 ? width / height : 16 / 9; - const isLandscape = aspectRatio > 1; - const showFullDesktopMode = aspectRatio > 1 && width > 1200; - const fullscreen = usePlayerStore((x) => x.fullscreen); - const { isPlayerRatioGreater: segIsLandscape } = useSegmentDimensions(); - - if ( - isPortraitLandscapeCase || - mode === "vod" || - showFullDesktopMode || - fullscreen || - (!isLandscape && !segIsLandscape) - ) { - return null; - } - - return ( - - - - ); -} - export function PlayerInner( props: Partial & { showChat: boolean; @@ -503,7 +434,7 @@ export function PlayerInner( {showFullDesktopMode || fullscreen ? ( ) : ( - (isLandscape || !!props.ingest) && ( + (isLandscape || props.mode === "vod") && ( )} - {props.mode === "vod" && ( - - - - - )} state.mode); const muteWasForced = usePlayerStore((state) => state.muteWasForced); const setMuteWasForced = usePlayerStore((state) => state.setMuteWasForced); const [playerIsReady, setPlayerIsReady] = useState(false); @@ -259,6 +260,20 @@ export function MobileUi({ /> )} + {mode === "vod" && ( + + + + + )} + String(n).padStart(2, "0"); - if (h > 0) return `${h}:${pad(m)}:${pad(sec)}`; - return `${m}:${pad(sec)}`; -} +import { View } from "../../ui/index"; export function SeekBar() { const mode = usePlayerStore((x) => x.mode); @@ -30,11 +20,9 @@ export function SeekBar() { const thumbHovered = useSharedValue(0); const thumbAnimStyle = useAnimatedStyle(() => ({ - boxShadow: `0 2px 10px rgba(0, 0, 0, ${withSpring(thumbHovered.value === 1 ? 1 : 0.18)})`, transform: [ - { scaleY: withSpring(thumbHovered.value === 1 ? 1 : 0.18) }, { - scaleX: withSpring(thumbHovered.value === 1 ? 1 : 0.35), + scale: withSpring(thumbHovered.value === 1 ? 1 : 0.18), }, ], })); @@ -69,7 +57,7 @@ export function SeekBar() { style={{ flex: 1, height: 90, - paddingBottom: 24, + paddingBottom: 10, }} > - - - {formatTime(displayTime)} - - - {formatTime(duration)} - - ); } diff --git a/js/components/src/components/mobile-player/ui/viewer-context-menu.tsx b/js/components/src/components/mobile-player/ui/viewer-context-menu.tsx index ff223f638..007c926cd 100644 --- a/js/components/src/components/mobile-player/ui/viewer-context-menu.tsx +++ b/js/components/src/components/mobile-player/ui/viewer-context-menu.tsx @@ -1,7 +1,3 @@ -import { - AppBskyActorDefs, - ComAtprotoModerationCreateReport, -} from "@atproto/api"; import { useRootContext } from "@rn-primitives/dropdown-menu"; import { Image } from "expo-image"; import { Cog } from "lucide-react-native"; @@ -15,15 +11,11 @@ import Animated, { import { ContentRights, ContentWarnings, - UpdateStreamTitleDialog, formatHandle, formatHandleWithAt, useAvatars, - useCanModerate, - useLivestream, useLivestreamInfo, useStreamplaceStore, - useUpdateLivestreamRecord, zero, } from "../../.."; import { useLivestreamStore } from "../../../livestream-store"; @@ -48,10 +40,6 @@ import { useTheme, } from "../../ui"; -type ReportSubject = - | ComAtprotoModerationCreateReport.InputSchema["subject"] - | null; - export function ContextMenu({ dropdownPortalContainer, }: { @@ -60,7 +48,11 @@ export function ContextMenu({ const th = useTheme(); const quality = usePlayerStore((x) => x.selectedRendition); const setQuality = usePlayerStore((x) => x.setSelectedRendition); - const qualities = useLivestreamStore((x) => x.renditions); + const mode = usePlayerStore((x) => x.mode); + const vodLevels = usePlayerStore((x) => x.vodLevels); + const playingVODRendition = usePlayerStore((x) => x.playingVODRendition); + const liveRenditions = useLivestreamStore((x) => x.renditions); + const qualities = mode === "vod" ? vodLevels : liveRenditions; const protocol = usePlayerStore((x) => x.protocol); const setProtocol = usePlayerStore((x) => x.setProtocol); @@ -111,12 +103,6 @@ export function ContextMenu({ const segment = useLivestreamStore((x) => x.segment); const [isOpen, setIsOpen] = useState(false); - const [showUpdateTitleDialog, setShowUpdateTitleDialog] = useState(false); - - const modPermissions = useCanModerate(profile?.did); - const { updateLivestream, isLoading: isUpdateTitleLoading } = - useUpdateLivestreamRecord(); - const livestreamRecord = useLivestream(); // Get content rights from the latest segment const contentRights = segment?.contentRights; @@ -152,273 +138,198 @@ export function ContextMenu({ // rerender when dropdown portal container changes so we swap portals 'seamlessly' return ( - <> - - - - - - - - {Platform.OS !== "web" && ( - - - {profile?.did && avatars[profile?.did]?.avatar && ( - + + + + + + + {Platform.OS !== "web" && ( + + + {profile?.did && avatars[profile?.did]?.avatar && ( + + )} + + + { + if (profile?.handle) { + const url = `https://bsky.app/profile/${formatHandle(profile)}`; + Linking.openURL(url); + } }} - style={{ width: 42, height: 42, borderRadius: 999 }} - contentFit="cover" - /> - )} - - - { - if (profile?.handle) { - const url = `https://bsky.app/profile/${formatHandle(profile)}`; - Linking.openURL(url); - } - }} - > - {profile && formatHandleWithAt(profile)} - - {/*{did && profile && ( + {profile && formatHandleWithAt(profile)} + + {/*{did && profile && ( )}*/} - - - {ls?.record.title || "Stream Title"} - - - - { - try { - if (graphManager.isFollowing) { - await graphManager.unfollow(); - } else { - await graphManager.follow(); - } - } catch (err) { - console.error("Follow/unfollow error:", err); - } - }} - > - {graphManager.isLoading - ? "Loading..." - : graphManager.isFollowing - ? "Unfollow" - : "Follow"} + {ls?.record.title || "Stream Title"} - - - { - if (profile?.handle) { - const url = `https://bsky.app/profile/${formatHandle(profile)}`; - Linking.openURL(url); + + + + { + try { + if (graphManager.isFollowing) { + await graphManager.unfollow(); + } else { + await graphManager.follow(); } - }} + } catch (err) { + console.error("Follow/unfollow error:", err); + } + }} + > + - View Profile on Bluesky - - - )} - - {Platform.OS !== "web" && modPermissions.canManageLivestream && ( - - - - )} - - - - - - Quality - - {quality === "source" - ? `Source${resolutionDisplay ? " " + resolutionDisplay + "\n" : ", "}` - : quality === "audio" - ? `Audio Only\n` - : quality} - {lowLatency ? "Low Latency" : ""} - - - - - - - - Source {resolutionDisplay} - - {qualities.map((r) => ( - - - {r.name === "audio" ? "Audio Only" : r.name} - - - ))} - - - - setLowLatency(!lowLatency)} - > - Low Latency - - - - - - - - setShowDebugInfo(!debugInfo)} + {graphManager.isLoading + ? "Loading..." + : graphManager.isFollowing + ? "Unfollow" + : "Follow"} + + + + { + if (profile?.handle) { + const url = `https://bsky.app/profile/${formatHandle(profile)}`; + Linking.openURL(url); + } + }} > - Show Debug Info - - - - - + View Profile on Bluesky + - - {contentWarnings && contentWarnings.length > 0 && ( - - - Stream may contain + )} + + + + + + Quality + + {quality === "source" + ? mode === "vod" + ? `Auto${playingVODRendition ? ` (${playingVODRendition})` : ""}\n` + : `Source${resolutionDisplay ? " " + resolutionDisplay + "\n" : ", "}` + : quality === "audio" + ? `Audio Only\n` + : quality} + {mode !== "vod" && lowLatency ? "Low Latency" : ""} - - )} - {contentRights && Object.keys(contentRights).length > 0 && ( - - )} - - - - {showUpdateTitleDialog && ( - setShowUpdateTitleDialog(false)} - /> - )} - - ); -} - -function UpdateStreamTitleItem({ - setShowUpdateTitleDialog, - isUpdateTitleLoading, - livestream, -}: { - setShowUpdateTitleDialog: (show: boolean) => void; - isUpdateTitleLoading: boolean; - livestream: any; -}) { - const { onOpenChange } = useRootContext(); - return ( - { - onOpenChange?.(false); - setShowUpdateTitleDialog(true); - }} - disabled={isUpdateTitleLoading || !livestream} - > - - {isUpdateTitleLoading ? "Updating..." : "Update stream title"} - - - ); -} - -function ReportUserItem({ - profile, - setReportModalOpen, - setReportSubject, -}: { - profile: AppBskyActorDefs.ProfileViewBasic | null; - setReportModalOpen: (open: boolean) => void; - setReportSubject: (subject: ReportSubject) => void; -}) { - const { onOpenChange } = useRootContext(); - return ( - { - if (!profile?.did) return; - onOpenChange?.(false); - setReportModalOpen(true); - setReportSubject({ - $type: "com.atproto.admin.defs#repoRef", - did: profile.did, - }); - }} - disabled={!profile?.did} - > - Report User... - + + + + + + Source {resolutionDisplay} + + {qualities.map((r) => ( + + {r.name === "audio" ? "Audio Only" : r.name} + + ))} + + + {mode !== "vod" && ( + <> + + setLowLatency(!lowLatency)} + > + Low Latency + + + + + )} + + + + + setShowDebugInfo(!debugInfo)} + > + Show Debug Info + + + + + + + {contentWarnings && contentWarnings.length > 0 && ( + + + Stream may contain + + + + )} + {contentRights && Object.keys(contentRights).length > 0 && ( + + )} + + + ); } diff --git a/js/components/src/components/mobile-player/ui/vod-controls.tsx b/js/components/src/components/mobile-player/ui/vod-controls.tsx index fc448d867..97388ee57 100644 --- a/js/components/src/components/mobile-player/ui/vod-controls.tsx +++ b/js/components/src/components/mobile-player/ui/vod-controls.tsx @@ -18,7 +18,7 @@ import { export function VodControls() { const mode = usePlayerStore((x) => x.mode); const status = usePlayerStore((x) => x.status); - const videoRef = usePlayerStore((x) => x.videoRef); + const togglePlayPause = usePlayerStore((x) => x.togglePlayPause); const quality = usePlayerStore((x) => x.selectedRendition); const setQuality = usePlayerStore((x) => x.setSelectedRendition); const liveRenditions = useLivestreamStore((x) => x.renditions); @@ -30,21 +30,6 @@ export function VodControls() { const isPlaying = status === PlayerStatus.PLAYING; - const togglePlayPause = () => { - if ( - videoRef && - typeof videoRef === "object" && - "current" in videoRef && - videoRef.current - ) { - if (videoRef.current.paused) { - videoRef.current.play(); - } else { - videoRef.current.pause(); - } - } - }; - return ( x.protocol); + const inProto = usePlayerStore((x) => x.protocol); + const selectedRendition = usePlayerStore((x) => x.selectedRendition); + const src = usePlayerStore((x) => x.src); const ingest = usePlayerStore((x) => x.ingestConnectionState) != null; + const { protocol } = srcToUrl({ src, selectedRendition }, inProto); return ( @@ -68,6 +71,7 @@ export function NativeVideo(props?: { }) { const videoRef = useRef(null); const protocol = usePlayerStore((x) => x.protocol); + const mode = usePlayerStore((x) => x.mode); const selectedRendition = usePlayerStore((x) => x.selectedRendition); const src = usePlayerStore((x) => x.src); @@ -82,6 +86,14 @@ export function NativeVideo(props?: { const setPlayerWidth = usePlayerStore((x) => x.setPlayerWidth); const setPlayerHeight = usePlayerStore((x) => x.setPlayerHeight); + const setPlayTime = usePlayerStore((x) => x.setPlayTime); + const setDuration = usePlayerStore((x) => x.setDuration); + const setBufferedEnd = usePlayerStore((x) => x.setBufferedEnd); + const playTime = usePlayerStore((x) => x.playTime); + const setTogglePlayPause = usePlayerStore((x) => x.setTogglePlayPause); + const status = usePlayerStore((x) => x.status); + const statusRef = useRef(status); + const lastReportedTimeRef = useRef(0); // State for live dimensions const [dimensions, setDimensions] = useState<{ @@ -106,15 +118,14 @@ export function NativeVideo(props?: { }, [setStatus]); const player = useVideoPlayer(url, (player) => { - player.addListener("playingChange", (newIsPlaying) => { - console.log("playingChange", newIsPlaying); - if (newIsPlaying) { + player.addListener("playingChange", ({ isPlaying }) => { + if (isPlaying) { setStatus(PlayerStatus.PLAYING); } else { setStatus(PlayerStatus.WAITING); } }); - player.loop = true; + player.loop = mode !== "vod"; player.muted = muted; player.play(); }); @@ -127,6 +138,51 @@ export function NativeVideo(props?: { player.volume = volume; }, [volume, player]); + useEffect(() => { + statusRef.current = status; + }, [status]); + + useEffect(() => { + player.loop = mode !== "vod"; + }, [player, mode]); + + useEffect(() => { + setTogglePlayPause(() => { + if (statusRef.current === PlayerStatus.PLAYING) { + player.pause(); + } else { + player.play(); + } + }); + return () => setTogglePlayPause(() => {}); + }, [player, setTogglePlayPause]); + + useEffect(() => { + if (mode !== "vod") return; + const interval = setInterval(() => { + const currentTime = player.currentTime; + const duration = player.duration; + lastReportedTimeRef.current = currentTime; + setPlayTime(currentTime); + if (isFinite(duration) && duration > 0) { + setDuration(duration); + } + const buffered = (player as any).bufferedPosition; + if (typeof buffered === "number" && buffered > 0) { + setBufferedEnd(buffered); + } + }, 250); + return () => clearInterval(interval); + }, [mode, player, setPlayTime, setDuration, setBufferedEnd]); + + useEffect(() => { + if (mode !== "vod") return; + const diff = Math.abs(playTime - lastReportedTimeRef.current); + if (diff > 1.0) { + player.seekBy(playTime - player.currentTime); + } + }, [playTime, mode, player]); + useEffect(() => { const subs = ( [ @@ -146,9 +202,8 @@ export function NativeVideo(props?: { }); subs.push( - player.addListener("playingChange", (newIsPlaying) => { - console.log("playingChange", newIsPlaying); - if (newIsPlaying) { + player.addListener("playingChange", ({ isPlaying }) => { + if (isPlaying) { setStatus(PlayerStatus.PLAYING); } else { setStatus(PlayerStatus.WAITING); diff --git a/js/components/src/components/mobile-player/video.tsx b/js/components/src/components/mobile-player/video.tsx index 1deffafc0..768d3b4c6 100644 --- a/js/components/src/components/mobile-player/video.tsx +++ b/js/components/src/components/mobile-player/video.tsx @@ -341,6 +341,9 @@ export function HLSPlayer(props: VideoProps) { const localRef = useRef(null); const hlsRef = useRef(null); const setVodLevels = usePlayerStore((x) => x.setVodLevels); + const setPlayingVODRendition = usePlayerStore( + (x) => x.setPlayingVODRendition, + ); const selectedRendition = usePlayerStore((x) => x.selectedRendition); const mode = usePlayerStore((x) => x.mode); @@ -382,10 +385,18 @@ export function HLSPlayer(props: VideoProps) { ); } }); + hls.on(Hls.Events.LEVEL_SWITCHED, (_, data) => { + const l = hls.levels[data.level]; + if (!l) return; + const name = + l.height > 0 ? `${l.height}p` : `${Math.round(l.bitrate / 1000)}k`; + setPlayingVODRendition(name); + }); return () => { hls.stopLoad(); hlsRef.current = null; setVodLevels([]); + setPlayingVODRendition(null); }; } else if (localRef.current.canPlayType("application/vnd.apple.mpegurl")) { localRef.current.src = props.url; diff --git a/js/components/src/player-store/player-state.tsx b/js/components/src/player-store/player-state.tsx index e01557b50..f824fea3c 100644 --- a/js/components/src/player-store/player-state.tsx +++ b/js/components/src/player-store/player-state.tsx @@ -41,6 +41,10 @@ export interface PlayerState { /** Quality levels parsed from HLS manifest (VOD only) */ vodLevels: Array<{ name: string }>; setVodLevels: (levels: Array<{ name: string }>) => void; + + /** Actual rendition name currently playing when in auto/source mode (web HLS only) */ + playingVODRendition: string | null; + setPlayingVODRendition: (name: string | null) => void; protocol: PlayerProtocol; setProtocol: (protocol: PlayerProtocol) => void; @@ -119,6 +123,9 @@ export interface PlayerState { /** Function to set the Picture-in-Picture action */ setPipAction: (action: (() => void) | undefined) => void; + togglePlayPause: () => void; + setTogglePlayPause: (fn: () => void) => void; + /** Player element width (CSS value or number) */ playerWidth?: string | number; /** Function to set the player width */ diff --git a/js/components/src/player-store/player-store.tsx b/js/components/src/player-store/player-store.tsx index 46fd58fa7..0d6aafc8b 100644 --- a/js/components/src/player-store/player-store.tsx +++ b/js/components/src/player-store/player-store.tsx @@ -29,6 +29,9 @@ export const makePlayerStore = (id?: string): StoreApi => { setBufferedEnd: (bufferedEnd) => set(() => ({ bufferedEnd })), vodLevels: [], setVodLevels: (vodLevels) => set(() => ({ vodLevels })), + playingVODRendition: null, + setPlayingVODRendition: (playingVODRendition) => + set(() => ({ playingVODRendition })), selectedRendition: "source", setSelectedRendition: (rendition: string) => @@ -126,6 +129,20 @@ export const makePlayerStore = (id?: string): StoreApi => { setPipAction: (action: (() => void) | undefined) => set(() => ({ pipAction: action })), + togglePlayPause: () => + set((state) => { + const ref = state.videoRef; + if (ref && typeof ref === "object" && "current" in ref && ref.current) { + if (ref.current.paused) { + ref.current.play(); + } else { + ref.current.pause(); + } + } + return {}; + }), + setTogglePlayPause: (fn) => set(() => ({ togglePlayPause: fn })), + // Player element width/height setters for global sync playerWidth: undefined, setPlayerWidth: (playerWidth: number) => set(() => ({ playerWidth })), -- 2.51.2 From b5c645f7a45dce310f288eb7b8695f90147a0d45 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Mon, 18 May 2026 15:33:04 -0500 Subject: [PATCH 03/47] readd demo url back, url query param --- js/app/components/mobile/player.tsx | 1 + js/app/src/linking-config.ts | 2 ++ js/app/src/navigation-helper.tsx | 1 + js/app/src/navigation-types.ts | 1 + js/app/src/screens/video.tsx | 11 ++++++++++- js/app/src/shell.tsx | 6 ++++++ 6 files changed, 21 insertions(+), 1 deletion(-) diff --git a/js/app/components/mobile/player.tsx b/js/app/components/mobile/player.tsx index 7622679b8..e09f5022e 100644 --- a/js/app/components/mobile/player.tsx +++ b/js/app/components/mobile/player.tsx @@ -276,6 +276,7 @@ function PlayerWithProvider( ) : ( !showUnavailable && diff --git a/js/app/src/linking-config.ts b/js/app/src/linking-config.ts index 5b06561be..858e3c8d1 100644 --- a/js/app/src/linking-config.ts +++ b/js/app/src/linking-config.ts @@ -128,6 +128,7 @@ export const SCREEN_PATHS = { PopoutInfoWidget: "widgets/info", PopoutMultistream: "widgets/multistream", PopoutLivestream: "widgets/livestream", + VodPlayerDemo: "vod-player-demo", } as const; /** @@ -232,6 +233,7 @@ export const streamplaceLinkingOptions: LinkingOptions + {/* Utility/embed screens */} Date: Tue, 19 May 2026 22:29:16 -0500 Subject: [PATCH 04/47] default to hls --- .../src/components/mobile-player/shared.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/js/components/src/components/mobile-player/shared.tsx b/js/components/src/components/mobile-player/shared.tsx index 15e2aca26..0c0fd575b 100644 --- a/js/components/src/components/mobile-player/shared.tsx +++ b/js/components/src/components/mobile-player/shared.tsx @@ -24,14 +24,10 @@ export function srcToUrl( if (props.src.startsWith("http://") || props.src.startsWith("https://")) { const segments = props.src.split(/[./]/); const suffix = segments[segments.length - 1]; - if (protocolSuffixes[suffix]) { - return { - url: props.src, - protocol: protocolSuffixes[suffix], - }; - } else { - throw new Error(`unknown playback protocol: ${suffix}`); - } + return { + url: props.src, + protocol: protocolSuffixes[suffix] ?? PlayerProtocol.HLS, + }; } let outUrl: string; if (protocol === PlayerProtocol.HLS) { -- 2.51.2 From a3cdb70ed5ee4a637703365a41e9a76b8c713eb6 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Wed, 20 May 2026 13:03:02 -0500 Subject: [PATCH 05/47] you can click behind the play button --- js/app/components/mobile/ui.tsx | 1 + .../components/mobile-player/ui/seek-bar.tsx | 11 +++- .../ui/viewer-loading-overlay.tsx | 61 +++++++++++++------ 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index 5c921bb52..81b1a24f5 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -168,6 +168,7 @@ export function MobileUi({ x.bufferedEnd); const seekTo = usePlayerStore((x) => x.seekTo); + const videoRef = usePlayerStore((x) => x.videoRef); const seekingRef = useRef(false); const [seekValue, setSeekValue] = useState(0); @@ -36,8 +37,16 @@ export function SeekBar() { if (seekingRef.current) { seekTo(seekValue); seekingRef.current = false; + if ( + videoRef && + typeof videoRef === "object" && + "current" in videoRef && + videoRef.current + ) { + videoRef.current.play().catch(() => {}); + } } - }, [seekValue, seekTo]); + }, [seekValue, seekTo, videoRef]); if (mode !== "vod" || duration <= 0) return null; diff --git a/js/components/src/components/mobile-player/ui/viewer-loading-overlay.tsx b/js/components/src/components/mobile-player/ui/viewer-loading-overlay.tsx index 26c99b2b8..a6d9b07d3 100644 --- a/js/components/src/components/mobile-player/ui/viewer-loading-overlay.tsx +++ b/js/components/src/components/mobile-player/ui/viewer-loading-overlay.tsx @@ -1,5 +1,6 @@ import { Play } from "lucide-react-native"; import { useEffect } from "react"; +import { Pressable } from "react-native"; import Animated, { useAnimatedStyle, useSharedValue, @@ -15,6 +16,7 @@ import { export function ViewerLoadingOverlay() { const status = usePlayerStore((x) => x.status); + const togglePlayPause = usePlayerStore((x) => x.togglePlayPause); const { theme, zero: zt } = useTheme(); const opacity = useSharedValue(0); @@ -40,27 +42,46 @@ export function ViewerLoadingOverlay() { return null; // No overlay when stopped } - let spinner = ; - if (status === PlayerStatus.PAUSE) { - spinner = ; - } + const isPaused = status === PlayerStatus.PAUSE; return ( - - {spinner} - + <> + + {!isPaused && } + + {isPaused && ( + + + + )} + ); } -- 2.51.2 From a9215d8caf29f9d9a5ef9ba748a0c4d279ceab80 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Tue, 26 May 2026 17:04:55 -0700 Subject: [PATCH 06/47] Scaffold videoStore and VideoProvider Add a per-video zustand store and provider mirroring the livestream-store / livestream-provider split. VideoProvider fetches place.stream.video metadata via place.stream.media.getVideo (using the possibly-unauthed node agent so VOD loads for logged-out viewers) and hydrates it into the store. Exposes useVideoStore plus useVideo / useVideoLoading / useVideoError convenience hooks. Co-Authored-By: Claude Opus 4.7 (1M context) --- js/components/src/index.tsx | 2 + js/components/src/video-provider/fetch.tsx | 47 +++++++++++++++++ js/components/src/video-provider/index.tsx | 51 +++++++++++++++++++ js/components/src/video-store/context.tsx | 8 +++ js/components/src/video-store/index.tsx | 4 ++ js/components/src/video-store/video-state.tsx | 19 +++++++ js/components/src/video-store/video-store.tsx | 39 ++++++++++++++ 7 files changed, 170 insertions(+) create mode 100644 js/components/src/video-provider/fetch.tsx create mode 100644 js/components/src/video-provider/index.tsx create mode 100644 js/components/src/video-store/context.tsx create mode 100644 js/components/src/video-store/index.tsx create mode 100644 js/components/src/video-store/video-state.tsx create mode 100644 js/components/src/video-store/video-store.tsx diff --git a/js/components/src/index.tsx b/js/components/src/index.tsx index cb1ca9221..55599cdd3 100644 --- a/js/components/src/index.tsx +++ b/js/components/src/index.tsx @@ -6,6 +6,8 @@ export * from "./livestream-store"; export * from "./player-store"; export * from "./streamplace-provider"; export * from "./streamplace-store"; +export * from "./video-provider"; +export * from "./video-store"; export { ErrorBoundary } from "./components/error-boundary"; diff --git a/js/components/src/video-provider/fetch.tsx b/js/components/src/video-provider/fetch.tsx new file mode 100644 index 000000000..bef5eea04 --- /dev/null +++ b/js/components/src/video-provider/fetch.tsx @@ -0,0 +1,47 @@ +import { useEffect, useRef } from "react"; +import { VideoViewHydrated } from "streamplace"; +import { usePossiblyUnauthedPDSAgent } from "../streamplace-store"; +import { getVideoStoreFromContext } from "../video-store"; + +// Fetches the place.stream.video metadata for the given AT-URI and pushes it +// into the surrounding VideoProvider's store. Refetches whenever the uri or +// the active agent changes. This is the VOD analogue of the livestream +// websocket consumer: instead of a long-lived socket, VOD metadata is a +// one-shot read that we hydrate into the store. +export function useVideoFetch(aturi: string) { + const store = getVideoStoreFromContext(); + const agent = usePossiblyUnauthedPDSAgent(); + const abortRef = useRef(null); + + useEffect(() => { + if (!agent || !aturi) { + return; + } + + // cancel any in-flight request from a previous uri/agent + abortRef.current?.abort(); + const abort = new AbortController(); + abortRef.current = abort; + + const { setAturi, setVideo, setLoading, setError } = store.getState(); + setAturi(aturi); + setLoading(true); + setError(null); + + agent.place.stream.media + .getVideo({ uri: aturi }, { signal: abort.signal }) + .then((res) => { + if (abort.signal.aborted) return; + setVideo(res.data as unknown as VideoViewHydrated); + setLoading(false); + }) + .catch((e) => { + if (abort.signal.aborted) return; + console.error("error fetching video", e); + setError(e?.message ?? "failed to load video"); + setLoading(false); + }); + + return () => abort.abort(); + }, [agent, aturi, store]); +} diff --git a/js/components/src/video-provider/index.tsx b/js/components/src/video-provider/index.tsx new file mode 100644 index 000000000..843b07169 --- /dev/null +++ b/js/components/src/video-provider/index.tsx @@ -0,0 +1,51 @@ +import React, { useContext, useRef } from "react"; +import { makeVideoStore } from "../video-store"; +import { VideoContext } from "../video-store/context"; +import { useVideoFetch } from "./fetch"; + +export function VideoProvider({ + children, + aturi, + ignoreOuterContext = false, +}: { + children: React.ReactNode; + aturi: string; + ignoreOuterContext?: boolean; +}) { + const context = useContext(VideoContext); + const store = useRef(makeVideoStore({ aturi })).current; + if (context) { + // this is ok, there's use cases for having one in another + // like having a player component that's independently usable + // but can also be embedded within an entire video page + if (!ignoreOuterContext) { + return <>{children}; + } + } + (window as any).videoStore = store; + return ( + + {children} + + ); +} + +export function VideoFetcher({ aturi }: { aturi: string }) { + useVideoFetch(aturi); + return <>; +} + +export function VideoPoller({ + children, + aturi, +}: { + children: React.ReactNode; + aturi: string; +}) { + return ( + <> + + {children} + + ); +} diff --git a/js/components/src/video-store/context.tsx b/js/components/src/video-store/context.tsx new file mode 100644 index 000000000..e608f5105 --- /dev/null +++ b/js/components/src/video-store/context.tsx @@ -0,0 +1,8 @@ +import { createContext } from "react"; +import { VideoStore } from "./video-store"; + +type VideoContextType = { + store: VideoStore; +}; + +export const VideoContext = createContext(null); diff --git a/js/components/src/video-store/index.tsx b/js/components/src/video-store/index.tsx new file mode 100644 index 000000000..37841bc95 --- /dev/null +++ b/js/components/src/video-store/index.tsx @@ -0,0 +1,4 @@ +// barrel file :) +export * from "./context"; +export * from "./video-state"; +export * from "./video-store"; diff --git a/js/components/src/video-store/video-state.tsx b/js/components/src/video-store/video-state.tsx new file mode 100644 index 000000000..29fd82f05 --- /dev/null +++ b/js/components/src/video-store/video-state.tsx @@ -0,0 +1,19 @@ +import { VideoViewHydrated } from "streamplace"; + +export interface VideoState { + // AT-URI of the place.stream.video record this store represents. + aturi: string; + setAturi: (aturi: string) => void; + + // Hydrated VOD metadata, or null until the first getVideo fetch resolves. + video: VideoViewHydrated | null; + setVideo: (video: VideoViewHydrated | null) => void; + + // True while a getVideo request is in flight. + loading: boolean; + setLoading: (loading: boolean) => void; + + // Last fetch error message, or null when the last fetch succeeded. + error: string | null; + setError: (error: string | null) => void; +} diff --git a/js/components/src/video-store/video-store.tsx b/js/components/src/video-store/video-store.tsx new file mode 100644 index 000000000..5a8a3cd9d --- /dev/null +++ b/js/components/src/video-store/video-store.tsx @@ -0,0 +1,39 @@ +import { useContext } from "react"; +import { createStore, StoreApi, useStore } from "zustand"; +import { VideoContext } from "./context"; +import { VideoState } from "./video-state"; + +export type VideoStore = StoreApi; + +export const makeVideoStore = ({ aturi }: { aturi: string }): VideoStore => { + return createStore()((set) => ({ + aturi, + setAturi: (aturi) => set({ aturi }), + video: null, + setVideo: (video) => set({ video }), + loading: false, + setLoading: (loading) => set({ loading }), + error: null, + setError: (error) => set({ error }), + })); +}; + +export function getVideoStoreFromContext(): VideoStore { + const context = useContext(VideoContext); + if (!context) { + throw new Error("useVideoStore must be used within a VideoProvider"); + } + return context.store; +} + +export function useVideoStore(selector: (state: VideoState) => U): U { + const store = getVideoStoreFromContext(); + return useStore(store, selector); +} + +/* Convenience selectors/hooks */ +export const useVideo = () => useVideoStore((x) => x.video); + +export const useVideoLoading = () => useVideoStore((x) => x.loading); + +export const useVideoError = () => useVideoStore((x) => x.error); -- 2.51.2 From 06559e05d76a0de752ae1fea4c1ccc542ea8d9a6 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Tue, 26 May 2026 17:55:21 -0700 Subject: [PATCH 07/47] wire up videoprovider stuff --- js/app/components/mobile/bottom-metadata.tsx | 37 +- js/app/components/mobile/player.tsx | 93 +- js/app/src/linking-config.ts | 2 +- js/app/src/screens/video.tsx | 30 +- js/components/package.json | 1 + .../src/components/mobile-player/shared.tsx | 14 + js/components/src/hooks/index.ts | 1 + js/components/src/hooks/useTitle.ts | 21 + .../src/livestream-store/livestream-store.tsx | 16 + js/components/src/video-store/video-store.tsx | 16 + js/streamplace/src/useful-types.ts | 5 + pkg/spxrpc/place_stream_playback_getvideo.go | 29 +- pnpm-lock.yaml | 1231 +++++++++++++---- 13 files changed, 1173 insertions(+), 323 deletions(-) create mode 100644 js/components/src/hooks/useTitle.ts diff --git a/js/app/components/mobile/bottom-metadata.tsx b/js/app/components/mobile/bottom-metadata.tsx index f26bc6047..2ca2699ab 100644 --- a/js/app/components/mobile/bottom-metadata.tsx +++ b/js/app/components/mobile/bottom-metadata.tsx @@ -14,7 +14,9 @@ import { useDID, useLivestreamInfo, useLivestreamStore, + usePlayerStore, useTheme, + useTitle, zero, } from "@streamplace/components"; import AQLink from "components/aqlink"; @@ -86,6 +88,7 @@ export function BottomMetadata({ const avatars = useAvatars(profile?.did ? [profile?.did] : []); const ls = useLivestreamStore((x) => x.livestream); const segment = useLivestreamStore((x) => x.segment); + const mode = usePlayerStore((x) => x.mode); const did = useDID(); const contentWarnings = @@ -188,7 +191,7 @@ export function BottomMetadata({ {/* Title */} - {ls?.record.title || "Stream Title"} + {useTitle()} {/* Activity + tags */} @@ -234,21 +237,23 @@ export function BottomMetadata({ - - - + {mode === "live" && ( + + + + )} diff --git a/js/app/components/mobile/player.tsx b/js/app/components/mobile/player.tsx index e09f5022e..997f3b22d 100644 --- a/js/app/components/mobile/player.tsx +++ b/js/app/components/mobile/player.tsx @@ -16,12 +16,13 @@ import { usePlayerStore, useSegment, useSegmentDimensions, + VideoProvider, View, } from "@streamplace/components"; import { gap, h, pt, w } from "@streamplace/components/src/lib/theme/atoms"; import { useLiveUser } from "hooks/useLiveUser"; import { useSidebarControl } from "hooks/useSidebarControl"; -import { ArrowLeft, ArrowRight } from "lucide-react-native"; +import { ArrowLeft } from "lucide-react-native"; import { ComponentRef, useEffect, useRef, useState } from "react"; import { Platform, @@ -55,16 +56,27 @@ export function Player( onTeleport?: (targetHandle: string, targetDID: string) => void; }, ) { - return ( + const inner = ( - - + ); + if (props.mode === "vod") { + return ( + + {inner} + + ); + } + if (props.mode === "live") { + return ( + {inner} + ); + } + throw new Error(`Unknown mode: ${props.mode}`); } function PlayerWithProvider( @@ -73,7 +85,10 @@ function PlayerWithProvider( onTeleport?: (targetHandle: string, targetDID: string) => void; }, ) { - const [showChat, setShowChat] = useState(true); + let [showChat, setShowChat] = useState(props.mode === "live"); + if (props.mode === "vod") { + showChat = false; + } const { shouldShowChatSidePanel, chatPanelWidth } = useResponsiveLayout(); const chatVisible = shouldShowChatSidePanel && showChat; const { width: screenWidth, height: screenHeight } = useWindowDimensions(); @@ -157,6 +172,30 @@ function PlayerWithProvider( const livestream = useLivestream(); const localLivestreamURI = useLivestreamStore((x) => x.localLivestreamURI); + let chatSection: React.ReactNode = null; + if (props.mode === "vod") { + chatSection = null; + } else if (isPortraitLandscapeCase) { + chatSection = ( + <> + + {!showUnavailable && ( + + )} + + ); + } else if (shouldShowChatSidePanel) { + chatSection = ( + + ); + } else if (!showUnavailable) { + chatSection = ; + } + if (isStreamingElsewhere) { return ( @@ -197,24 +236,7 @@ function PlayerWithProvider( Back - {userProfile?.did && ( - - )} + {chatSection} ); @@ -265,22 +287,7 @@ function PlayerWithProvider( showUnavailable={showUnavailable} /> - {isPortraitLandscapeCase ? ( - <> - - {!showUnavailable && ( - - )} - - ) : shouldShowChatSidePanel ? ( - - ) : ( - !showUnavailable && - )} + {chatSection} diff --git a/js/app/src/linking-config.ts b/js/app/src/linking-config.ts index 858e3c8d1..c8a680181 100644 --- a/js/app/src/linking-config.ts +++ b/js/app/src/linking-config.ts @@ -128,7 +128,7 @@ export const SCREEN_PATHS = { PopoutInfoWidget: "widgets/info", PopoutMultistream: "widgets/multistream", PopoutLivestream: "widgets/livestream", - VodPlayerDemo: "vod-player-demo", + VodPlayerDemo: ":user/video/:tid", } as const; /** diff --git a/js/app/src/screens/video.tsx b/js/app/src/screens/video.tsx index bff14d446..dca7ed4c8 100644 --- a/js/app/src/screens/video.tsx +++ b/js/app/src/screens/video.tsx @@ -1,44 +1,36 @@ -import { Text, useStreamplaceStore, View, zero } from "@streamplace/components"; -import { Platform } from "react-native"; +import { useStreamplaceStore, View, zero } from "@streamplace/components"; +import { Redirect } from "components/aqlink"; import { Player } from "../../components/mobile/player"; const BBB_HLS_URL = "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8"; -function usePlaybackUrl(src?: string): string { +function usePlaybackUrl(user: string, tid: string): string { const serverUrl = useStreamplaceStore((x) => x.url); - if (!src) return BBB_HLS_URL; - if (src.startsWith("http://") || src.startsWith("https://")) return src; - return `${serverUrl}/api/playback/${src}/hls/index.m3u8`; -} - -function useQuerySrc(routeSrc?: string): string | undefined { - if (Platform.OS === "web") { - const qUrl = new URLSearchParams(window.location.search).get("url"); - if (qUrl) return qUrl; - } - return routeSrc; + return `at://${user}/place.stream.video/${tid}`; } export default function VideoScreen({ route, }: { - route?: { params?: { src?: string } }; + route?: { params?: { user?: string; tid?: string } }; }) { - const src = useQuerySrc(route?.params?.src); - const url = usePlaybackUrl(src); + if (!route?.params?.user || !route?.params?.tid) { + return ; + } + const url = usePlaybackUrl(route.params.user, route.params.tid); return ( - {src && ( + {/*{src && ( {src} - )} + )}*/} ); } diff --git a/js/components/package.json b/js/components/package.json index 0f543e82c..9f20f50d8 100644 --- a/js/components/package.json +++ b/js/components/package.json @@ -28,6 +28,7 @@ "dependencies": { "@atproto/api": "^0.19.3", "@atproto/crypto": "^0.4.5", + "@atproto/syntax": "^0.6.1", "@emoji-mart/react": "^1.1.1", "@fluent/bundle": "^0.19.1", "@fluent/langneg": "^0.7.0", diff --git a/js/components/src/components/mobile-player/shared.tsx b/js/components/src/components/mobile-player/shared.tsx index 0c0fd575b..972b0c5e0 100644 --- a/js/components/src/components/mobile-player/shared.tsx +++ b/js/components/src/components/mobile-player/shared.tsx @@ -1,3 +1,4 @@ +import { AtUri } from "@atproto/syntax"; import { useMemo } from "react"; import { PlayerProtocol } from "../../player-store/player-state"; import { useStreamplaceStore } from "../../streamplace-store"; @@ -21,6 +22,19 @@ export function srcToUrl( } { const url = useStreamplaceStore((x) => x.url); return useMemo(() => { + if (props.src.startsWith("at://")) { + const aturi = new AtUri(props.src); + if (aturi.collection === "place.stream.video") { + return { + url: `${url}/xrpc/place.stream.playback.getVideoPlaylist?uri=${props.src}`, + protocol: PlayerProtocol.HLS, + }; + } else { + throw new Error( + `this player doesn't know how to play ${aturi.collection} records (${props.src})`, + ); + } + } if (props.src.startsWith("http://") || props.src.startsWith("https://")) { const segments = props.src.split(/[./]/); const suffix = segments[segments.length - 1]; diff --git a/js/components/src/hooks/index.ts b/js/components/src/hooks/index.ts index 17621c262..b436308ad 100644 --- a/js/components/src/hooks/index.ts +++ b/js/components/src/hooks/index.ts @@ -11,3 +11,4 @@ export * from "./usePlayerDimensions"; export * from "./usePointerDevice"; export * from "./useSegmentDimensions"; export * from "./useSegmentTiming"; +export * from "./useTitle"; diff --git a/js/components/src/hooks/useTitle.ts b/js/components/src/hooks/useTitle.ts new file mode 100644 index 000000000..8d847f0ba --- /dev/null +++ b/js/components/src/hooks/useTitle.ts @@ -0,0 +1,21 @@ +import { useLivestreamStoreOptional } from "../livestream-store"; +import { usePlayerStore } from "../player-store"; +import { useVideoStoreOptional } from "../video-store"; + +// Surfaces the human-readable title for whatever the player is currently +// showing, transparently switching between the live stream record and the +// VOD record based on the player's mode. This lets player UI stay generic: +// `const title = useTitle()` works the same in live and vod contexts. +// +// Reads from the *optional* store hooks so it's safe to call even when only +// one of the providers (LivestreamProvider / VideoProvider) is mounted. +// Returns undefined when no title is available yet (e.g. metadata still +// loading) so callers can supply their own fallback. +export function useTitle(): string | undefined { + const mode = usePlayerStore((x) => x.mode); + const liveTitle = useLivestreamStoreOptional( + (x) => x.livestream?.record.title, + ); + const vodTitle = useVideoStoreOptional((x) => x.video?.record.title); + return mode === "vod" ? vodTitle : liveTitle; +} diff --git a/js/components/src/livestream-store/livestream-store.tsx b/js/components/src/livestream-store/livestream-store.tsx index ed8e935c0..f29306b11 100644 --- a/js/components/src/livestream-store/livestream-store.tsx +++ b/js/components/src/livestream-store/livestream-store.tsx @@ -54,6 +54,22 @@ export function useLivestreamStore( return useStore(store, selector); } +// A shared, never-populated store used as a fallback so the optional hook +// below can call useStore unconditionally even when no LivestreamProvider is +// mounted (e.g. when the player is in "vod" mode). +const emptyLivestreamStore = makeLivestreamStore(); + +// Like useLivestreamStore, but reads against an empty store instead of +// throwing when there's no LivestreamProvider in the tree. Useful for +// mode-generic hooks (see useTitle) that may run outside a livestream context. +export function useLivestreamStoreOptional( + selector: (state: LivestreamState) => U, +): U { + const context = useContext(LivestreamContext); + const store = context?.store ?? emptyLivestreamStore; + return useStore(store, selector); +} + export const useHandleWebsocketMessages = () => { const store = getStoreFromContext(); return (messages: any[]) => { diff --git a/js/components/src/video-store/video-store.tsx b/js/components/src/video-store/video-store.tsx index 5a8a3cd9d..995552021 100644 --- a/js/components/src/video-store/video-store.tsx +++ b/js/components/src/video-store/video-store.tsx @@ -31,6 +31,22 @@ export function useVideoStore(selector: (state: VideoState) => U): U { return useStore(store, selector); } +// A shared, never-populated store used as a fallback so the optional hook +// below can call useStore unconditionally even when no VideoProvider is +// mounted (e.g. when the player is in "live" mode). +const emptyVideoStore = makeVideoStore({ aturi: "" }); + +// Like useVideoStore, but reads against an empty store instead of throwing +// when there's no VideoProvider in the tree. Useful for mode-generic hooks +// (see useTitle) that may run outside a video context. +export function useVideoStoreOptional( + selector: (state: VideoState) => U, +): U { + const context = useContext(VideoContext); + const store = context?.store ?? emptyVideoStore; + return useStore(store, selector); +} + /* Convenience selectors/hooks */ export const useVideo = () => useVideoStore((x) => x.video); diff --git a/js/streamplace/src/useful-types.ts b/js/streamplace/src/useful-types.ts index 8ddf81c44..230bf200d 100644 --- a/js/streamplace/src/useful-types.ts +++ b/js/streamplace/src/useful-types.ts @@ -3,6 +3,7 @@ import { PlaceStreamChatMessage, PlaceStreamChatPinnedRecord, PlaceStreamLivestream, + PlaceStreamMediaGetVideo, } from "./lexicons"; export interface LivestreamViewHydrated @@ -19,3 +20,7 @@ export interface PinnedRecordViewHydrated extends PlaceStreamChatDefs.PinnedRecordView { record: PlaceStreamChatPinnedRecord.Record; } + +export interface VideoViewHydrated extends PlaceStreamMediaGetVideo.VideoView { + record: PlaceStreamLivestream.Record; +} diff --git a/pkg/spxrpc/place_stream_playback_getvideo.go b/pkg/spxrpc/place_stream_playback_getvideo.go index 38403b0b0..881abd1fc 100644 --- a/pkg/spxrpc/place_stream_playback_getvideo.go +++ b/pkg/spxrpc/place_stream_playback_getvideo.go @@ -273,12 +273,29 @@ func (s *Server) HandleGetVideoPlaylist(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("UnsupportedCollection: %s", aturi.Collection())) } - // The authority must be a DID, not a handle: media playlists embed - // it in every getVideoBlob URL for egress accounting, and we want - // attribution keyed on the stable identifier. - ownerDID, err := aturi.Authority().AsDID() - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "uri authority must be a DID: "+err.Error()) + var ownerDID syntax.DID + if aturi.Authority().IsHandle() { + handle, err := aturi.Authority().AsHandle() + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "failed to convert handle: "+err.Error()) + } + res, err := s.resolveStreamer(ctx, handle.String()) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "failed to resolve streamer: "+err.Error()) + } + ownerDID, err = syntax.ParseDID(res) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "resolved streamer must be a valid DID: "+err.Error()) + } + aturi, err = syntax.ParseATURI(fmt.Sprintf("at://%s/%s", ownerDID.String(), aturi.Path())) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "failed to parse AT-URI: "+err.Error()) + } + } else { + ownerDID, err = aturi.Authority().AsDID() + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "uri authority must be a DID: "+err.Error()) + } } if s.playbackStore == nil { return echo.NewHTTPError(http.StatusServiceUnavailable, "playback store not configured") diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2cfb36ee6..e89e6637c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -399,9 +399,12 @@ importers: '@atproto/crypto': specifier: ^0.4.5 version: 0.4.5 + '@atproto/syntax': + specifier: ^0.6.1 + version: 0.6.1 '@emoji-mart/react': specifier: ^1.1.1 - version: 1.1.1(emoji-mart@5.6.0)(react@19.2.0) + version: 1.1.1(emoji-mart@5.6.0)(react@19.2.4) '@fluent/bundle': specifier: ^0.19.1 version: 0.19.1 @@ -410,43 +413,43 @@ importers: version: 0.7.0 '@fluent/react': specifier: ^0.15.2 - version: 0.15.2(@fluent/bundle@0.19.1)(react@19.2.0) + version: 0.15.2(@fluent/bundle@0.19.1)(react@19.2.4) '@gorhom/bottom-sheet': specifier: ^5.2.8 - version: 5.2.8(@types/react@19.2.14)(react-native-gesture-handler@2.30.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native-reanimated@4.2.2(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) + version: 5.2.8(@types/react@19.2.14)(react-native-gesture-handler@2.30.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native-reanimated@4.2.2(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) '@radix-ui/react-dropdown-menu': specifier: ^2.1.16 - version: 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) '@rn-primitives/dropdown-menu': specifier: ^1.2.0 - version: 1.2.0(@rn-primitives/portal@1.3.0(@types/react@19.2.14)(immer@10.1.1)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) + version: 1.2.0(@rn-primitives/portal@1.3.0(@types/react@19.2.14)(immer@10.1.1)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) '@rn-primitives/portal': specifier: ^1.3.0 - version: 1.3.0(@types/react@19.2.14)(immer@10.1.1)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) + version: 1.3.0(@types/react@19.2.14)(immer@10.1.1)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) '@rn-primitives/slider': specifier: ^1.2.0 - version: 1.2.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) + version: 1.2.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) class-variance-authority: specifier: ^0.6.1 version: 0.6.1 expo-image: specifier: ^55.0.6 - version: 55.0.6(expo@55.0.6)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) + version: 55.0.6(expo@55.0.6)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) expo-keep-awake: specifier: ^55.0.4 - version: 55.0.4(expo@55.0.6)(react@19.2.0) + version: 55.0.4(expo@55.0.6)(react@19.2.4) expo-localization: specifier: '*' version: 55.0.8(expo@55.0.6)(react@19.2.0) expo-screen-orientation: specifier: ^55.0.8 - version: 55.0.8(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10)) + version: 55.0.8(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10)) expo-sqlite: specifier: ^55.0.10 - version: 55.0.10(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) + version: 55.0.10(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) expo-video: specifier: ^55.0.10 - version: 55.0.10(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) + version: 55.0.10(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) graphemer: specifier: ^1.4.0 version: 1.4.0 @@ -473,34 +476,34 @@ importers: version: 2.0.1 lucide-react-native: specifier: ^0.562.0 - version: 0.562.0(react-native-svg@15.15.3(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) + version: 0.562.0(react-native-svg@15.15.3(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) react: specifier: '*' - version: 19.2.0 + version: 19.2.4 react-i18next: specifier: ^15.7.3 - version: 15.7.4(i18next@25.8.18(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3) + version: 15.7.4(i18next@25.8.18(typescript@5.9.3))(react-dom@19.2.0(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3) react-native: specifier: ^0.83.2 - version: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + version: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) react-native-edge-to-edge: specifier: ^1.6.2 - version: 1.8.1(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) + version: 1.8.1(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) react-native-gesture-handler: specifier: ^2.30.0 - version: 2.30.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) + version: 2.30.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) react-native-reanimated: specifier: ~4.2.1 - version: 4.2.2(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) + version: 4.2.2(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) react-native-safe-area-context: specifier: ^5.7.0 - version: 5.7.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) + version: 5.7.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) react-native-svg: specifier: ^15.15.3 - version: 15.15.3(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) + version: 15.15.3(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) react-native-webrtc: specifier: git+https://github.com/streamplace/react-native-webrtc.git#74fa32266e3a2fee180f5e01bb8753af2a92d9d3 - version: https://codeload.github.com/streamplace/react-native-webrtc/tar.gz/74fa32266e3a2fee180f5e01bb8753af2a92d9d3(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10)) + version: https://codeload.github.com/streamplace/react-native-webrtc/tar.gz/74fa32266e3a2fee180f5e01bb8753af2a92d9d3(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10)) react-use-websocket: specifier: ^4.13.0 version: 4.13.0 @@ -512,7 +515,7 @@ importers: version: 2.47.2(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6) zustand: specifier: ^5.0.5 - version: 5.0.11(@types/react@19.2.14)(immer@10.1.1)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) + version: 5.0.11(@types/react@19.2.14)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) devDependencies: '@fluent/syntax': specifier: ^0.19.0 @@ -525,7 +528,7 @@ importers: version: 2.15.0 i18next-cli: specifier: ^1.32.0 - version: 1.49.7(@swc/helpers@0.5.17)(@types/node@25.5.0)(i18next@25.8.18(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(typescript@5.9.3) + version: 1.49.7(@swc/helpers@0.5.17)(@types/node@25.5.0)(i18next@25.8.18(typescript@5.9.3))(react-dom@19.2.0(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.9.3) nodemon: specifier: ^3.1.10 version: 3.1.14 @@ -963,6 +966,10 @@ packages: '@atproto/syntax@0.5.0': resolution: {integrity: sha512-UA2DSpGdOQzUQ4gi5SH+NEJz/YR3a3Fg3y2oh+xETDSiTRmA4VhHRCojhXAVsBxUT6EnItw190C/KN+DWW90kw==} + '@atproto/syntax@0.6.1': + resolution: {integrity: sha512-kA4dQDoMPpWCH8N0Q4KoSq024u5MkVfDVa8DdhyLjGA72z/khbOf1jXKPv7NIL2oEc9aj7geKELdvqyf4ogopA==} + engines: {node: '>=22'} + '@atproto/ws-client@0.0.4': resolution: {integrity: sha512-dox1XIymuC7/ZRhUqKezIGgooZS45C6vHCfu0PnWjfvsLCK2kAlnvX4IBkA/WpcoijDhQ9ejChnFbo/sLmgvAg==} engines: {node: '>=18.7.0'} @@ -15794,6 +15801,11 @@ snapshots: dependencies: tslib: 2.8.1 + '@atproto/syntax@0.6.1': + dependencies: + iso-datestring-validator: 2.2.2 + tslib: 2.8.1 + '@atproto/ws-client@0.0.4(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@atproto/common': 0.5.14 @@ -17628,7 +17640,7 @@ snapshots: '@config-plugins/react-native-webrtc@10.0.0(expo@55.0.6)': dependencies: - expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3)(utf-8-validate@5.0.10) '@croct/json5-parser@0.2.2': dependencies: @@ -18198,10 +18210,10 @@ snapshots: tslib: 2.8.1 optional: true - '@emoji-mart/react@1.1.1(emoji-mart@5.6.0)(react@19.2.0)': + '@emoji-mart/react@1.1.1(emoji-mart@5.6.0)(react@19.2.4)': dependencies: emoji-mart: 5.6.0 - react: 19.2.0 + react: 19.2.4 '@esbuild/aix-ppc64@0.25.12': optional: true @@ -18635,6 +18647,156 @@ snapshots: - typescript - utf-8-validate + '@expo/cli@55.0.16(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(expo-constants@55.0.7(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.9.3))(expo-font@55.0.4(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(expo@55.0.6)(react-dom@19.2.0(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@expo/code-signing-certificates': 0.0.6 + '@expo/config': 55.0.8(typescript@5.9.3) + '@expo/config-plugins': 55.0.6 + '@expo/devcert': 1.2.1 + '@expo/env': 2.1.1 + '@expo/image-utils': 0.8.12 + '@expo/json-file': 10.0.12 + '@expo/log-box': 55.0.7(@expo/dom-webview@55.0.3)(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + '@expo/metro': 54.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@expo/metro-config': 55.0.9(bufferutil@4.0.8)(expo@55.0.6)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@expo/osascript': 2.4.2 + '@expo/package-manager': 1.10.3 + '@expo/plist': 0.5.2 + '@expo/prebuild-config': 55.0.8(expo@55.0.6)(typescript@5.9.3) + '@expo/require-utils': 55.0.2(typescript@5.9.3) + '@expo/router-server': 55.0.10(@expo/metro-runtime@55.0.6)(expo-constants@55.0.7(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.9.3))(expo-font@55.0.4(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(expo-server@55.0.6)(expo@55.0.6)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@expo/schema-utils': 55.0.2 + '@expo/spawn-async': 1.7.2 + '@expo/ws-tunnel': 1.0.6 + '@expo/xcpretty': 4.4.1 + '@react-native/dev-middleware': 0.83.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) + accepts: 1.3.8 + arg: 5.0.2 + better-opn: 3.0.2 + bplist-creator: 0.1.0 + bplist-parser: 0.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + compression: 1.8.1 + connect: 3.7.0 + debug: 4.4.3 + dnssd-advertise: 1.1.3 + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-server: 55.0.6 + fetch-nodeshim: 0.4.9 + getenv: 2.0.0 + glob: 13.0.6 + lan-network: 0.2.0 + multitars: 0.2.4 + node-forge: 1.3.3 + npm-package-arg: 11.0.3 + ora: 3.4.0 + picomatch: 4.0.3 + pretty-format: 29.7.0 + progress: 2.0.3 + prompts: 2.4.2 + resolve-from: 5.0.0 + semver: 7.7.4 + send: 0.19.2 + slugify: 1.6.6 + source-map-support: 0.5.21 + stacktrace-parser: 0.1.11 + structured-headers: 0.4.1 + terminal-link: 2.1.1 + toqr: 0.1.1 + wrap-ansi: 7.0.0 + ws: 8.19.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + zod: 3.25.76 + optionalDependencies: + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - '@expo/dom-webview' + - '@expo/metro-runtime' + - bufferutil + - expo-constants + - expo-font + - react + - react-dom + - react-server-dom-webpack + - supports-color + - typescript + - utf-8-validate + + '@expo/cli@55.0.16(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(expo-constants@55.0.7(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.3.3))(expo-font@55.0.4(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(expo@55.0.6)(react-dom@19.2.0(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3)(utf-8-validate@5.0.10)': + dependencies: + '@expo/code-signing-certificates': 0.0.6 + '@expo/config': 55.0.8(typescript@5.3.3) + '@expo/config-plugins': 55.0.6 + '@expo/devcert': 1.2.1 + '@expo/env': 2.1.1 + '@expo/image-utils': 0.8.12 + '@expo/json-file': 10.0.12 + '@expo/log-box': 55.0.7(@expo/dom-webview@55.0.3)(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + '@expo/metro': 54.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@expo/metro-config': 55.0.9(bufferutil@4.0.8)(expo@55.0.6)(typescript@5.3.3)(utf-8-validate@5.0.10) + '@expo/osascript': 2.4.2 + '@expo/package-manager': 1.10.3 + '@expo/plist': 0.5.2 + '@expo/prebuild-config': 55.0.8(expo@55.0.6)(typescript@5.3.3) + '@expo/require-utils': 55.0.2(typescript@5.3.3) + '@expo/router-server': 55.0.10(@expo/metro-runtime@55.0.6)(expo-constants@55.0.7(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.3.3))(expo-font@55.0.4(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(expo-server@55.0.6)(expo@55.0.6)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@expo/schema-utils': 55.0.2 + '@expo/spawn-async': 1.7.2 + '@expo/ws-tunnel': 1.0.6 + '@expo/xcpretty': 4.4.1 + '@react-native/dev-middleware': 0.83.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) + accepts: 1.3.8 + arg: 5.0.2 + better-opn: 3.0.2 + bplist-creator: 0.1.0 + bplist-parser: 0.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + compression: 1.8.1 + connect: 3.7.0 + debug: 4.4.3 + dnssd-advertise: 1.1.3 + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3)(utf-8-validate@5.0.10) + expo-server: 55.0.6 + fetch-nodeshim: 0.4.9 + getenv: 2.0.0 + glob: 13.0.6 + lan-network: 0.2.0 + multitars: 0.2.4 + node-forge: 1.3.3 + npm-package-arg: 11.0.3 + ora: 3.4.0 + picomatch: 4.0.3 + pretty-format: 29.7.0 + progress: 2.0.3 + prompts: 2.4.2 + resolve-from: 5.0.0 + semver: 7.7.4 + send: 0.19.2 + slugify: 1.6.6 + source-map-support: 0.5.21 + stacktrace-parser: 0.1.11 + structured-headers: 0.4.1 + terminal-link: 2.1.1 + toqr: 0.1.1 + wrap-ansi: 7.0.0 + ws: 8.19.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + zod: 3.25.76 + optionalDependencies: + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - '@expo/dom-webview' + - '@expo/metro-runtime' + - bufferutil + - expo-constants + - expo-font + - react + - react-dom + - react-server-dom-webpack + - supports-color + - typescript + - utf-8-validate + '@expo/code-signing-certificates@0.0.6': dependencies: node-forge: 1.3.3 @@ -18659,6 +18821,23 @@ snapshots: '@expo/config-types@55.0.5': {} + '@expo/config@55.0.8(typescript@5.3.3)': + dependencies: + '@expo/config-plugins': 55.0.6 + '@expo/config-types': 55.0.5 + '@expo/json-file': 10.0.12 + '@expo/require-utils': 55.0.2(typescript@5.3.3) + deepmerge: 4.3.1 + getenv: 2.0.0 + glob: 13.0.6 + resolve-from: 5.0.0 + resolve-workspace-root: 2.0.0 + semver: 7.7.1 + slugify: 1.6.6 + transitivePeerDependencies: + - supports-color + - typescript + '@expo/config@55.0.8(typescript@5.9.3)': dependencies: '@expo/config-plugins': 55.0.6 @@ -18690,12 +18869,38 @@ snapshots: react: 19.2.0 react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + '@expo/devtools@55.0.2(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': + dependencies: + chalk: 4.1.2 + optionalDependencies: + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + + '@expo/devtools@55.0.2(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': + dependencies: + chalk: 4.1.2 + optionalDependencies: + react: 19.2.4 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + '@expo/dom-webview@55.0.3(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)': dependencies: expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10) react: 19.2.0 react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + '@expo/dom-webview@55.0.3(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': + dependencies: + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + + '@expo/dom-webview@55.0.3(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': + dependencies: + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3)(utf-8-validate@5.0.10) + react: 19.2.4 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + '@expo/env@2.1.1': dependencies: chalk: 4.1.2 @@ -18735,6 +18940,14 @@ snapshots: '@babel/code-frame': 7.29.0 json5: 2.2.3 + '@expo/local-build-cache-provider@55.0.6(typescript@5.3.3)': + dependencies: + '@expo/config': 55.0.8(typescript@5.3.3) + chalk: 4.1.2 + transitivePeerDependencies: + - supports-color + - typescript + '@expo/local-build-cache-provider@55.0.6(typescript@5.9.3)': dependencies: '@expo/config': 55.0.8(typescript@5.9.3) @@ -18752,6 +18965,53 @@ snapshots: react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) stacktrace-parser: 0.1.10 + '@expo/log-box@55.0.7(@expo/dom-webview@55.0.3)(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': + dependencies: + '@expo/dom-webview': 55.0.3(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + anser: 1.4.10 + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + stacktrace-parser: 0.1.10 + + '@expo/log-box@55.0.7(@expo/dom-webview@55.0.3)(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': + dependencies: + '@expo/dom-webview': 55.0.3(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + anser: 1.4.10 + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3)(utf-8-validate@5.0.10) + react: 19.2.4 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + stacktrace-parser: 0.1.10 + + '@expo/metro-config@55.0.9(bufferutil@4.0.8)(expo@55.0.6)(typescript@5.3.3)(utf-8-validate@5.0.10)': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@expo/config': 55.0.8(typescript@5.3.3) + '@expo/env': 2.1.1 + '@expo/json-file': 10.0.12 + '@expo/metro': 54.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@expo/spawn-async': 1.7.2 + browserslist: 4.28.1 + chalk: 4.1.2 + debug: 4.4.3 + getenv: 2.0.0 + glob: 13.0.6 + hermes-parser: 0.32.1 + jsc-safe-url: 0.2.4 + lightningcss: 1.31.1 + picomatch: 4.0.3 + postcss: 8.4.47 + resolve-from: 5.0.0 + optionalDependencies: + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - supports-color + - typescript + - utf-8-validate + '@expo/metro-config@55.0.9(bufferutil@4.0.8)(expo@55.0.6)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@babel/code-frame': 7.29.0 @@ -18774,7 +19034,7 @@ snapshots: postcss: 8.4.47 resolve-from: 5.0.0 optionalDependencies: - expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - supports-color @@ -18796,6 +19056,38 @@ snapshots: transitivePeerDependencies: - '@expo/dom-webview' + '@expo/metro-runtime@55.0.6(@expo/dom-webview@55.0.3)(expo@55.0.6)(react-dom@19.2.0(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': + dependencies: + '@expo/log-box': 55.0.7(@expo/dom-webview@55.0.3)(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + anser: 1.4.10 + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) + pretty-format: 29.7.0 + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + stacktrace-parser: 0.1.10 + whatwg-fetch: 3.6.20 + optionalDependencies: + react-dom: 19.2.0(react@19.2.4) + transitivePeerDependencies: + - '@expo/dom-webview' + optional: true + + '@expo/metro-runtime@55.0.6(@expo/dom-webview@55.0.3)(expo@55.0.6)(react-dom@19.2.0(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': + dependencies: + '@expo/log-box': 55.0.7(@expo/dom-webview@55.0.3)(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + anser: 1.4.10 + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3)(utf-8-validate@5.0.10) + pretty-format: 29.7.0 + react: 19.2.4 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + stacktrace-parser: 0.1.10 + whatwg-fetch: 3.6.20 + optionalDependencies: + react-dom: 19.2.0(react@19.2.4) + transitivePeerDependencies: + - '@expo/dom-webview' + optional: true + '@expo/metro@54.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: metro: 0.83.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -18836,6 +19128,23 @@ snapshots: base64-js: 1.5.1 xmlbuilder: 15.1.1 + '@expo/prebuild-config@55.0.8(expo@55.0.6)(typescript@5.3.3)': + dependencies: + '@expo/config': 55.0.8(typescript@5.3.3) + '@expo/config-plugins': 55.0.6 + '@expo/config-types': 55.0.5 + '@expo/image-utils': 0.8.12 + '@expo/json-file': 10.0.12 + '@react-native/normalize-colors': 0.83.2 + debug: 4.4.3 + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3)(utf-8-validate@5.0.10) + resolve-from: 5.0.0 + semver: 7.7.1 + xml2js: 0.6.0 + transitivePeerDependencies: + - supports-color + - typescript + '@expo/prebuild-config@55.0.8(expo@55.0.6)(typescript@5.9.3)': dependencies: '@expo/config': 55.0.8(typescript@5.9.3) @@ -18845,7 +19154,7 @@ snapshots: '@expo/json-file': 10.0.12 '@react-native/normalize-colors': 0.83.2 debug: 4.4.3 - expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) resolve-from: 5.0.0 semver: 7.7.1 xml2js: 0.6.0 @@ -18853,6 +19162,16 @@ snapshots: - supports-color - typescript + '@expo/require-utils@55.0.2(typescript@5.3.3)': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/core': 7.29.0 + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.29.0) + optionalDependencies: + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + '@expo/require-utils@55.0.2(typescript@5.9.3)': dependencies: '@babel/code-frame': 7.29.0 @@ -18877,6 +19196,34 @@ snapshots: transitivePeerDependencies: - supports-color + '@expo/router-server@55.0.10(@expo/metro-runtime@55.0.6)(expo-constants@55.0.7(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.9.3))(expo-font@55.0.4(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(expo-server@55.0.6)(expo@55.0.6)(react-dom@19.2.0(react@19.2.4))(react@19.2.4)': + dependencies: + debug: 4.4.3 + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-constants: 55.0.7(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.9.3) + expo-font: 55.0.4(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + expo-server: 55.0.6 + react: 19.2.4 + optionalDependencies: + '@expo/metro-runtime': 55.0.6(@expo/dom-webview@55.0.3)(expo@55.0.6)(react-dom@19.2.0(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + react-dom: 19.2.0(react@19.2.4) + transitivePeerDependencies: + - supports-color + + '@expo/router-server@55.0.10(@expo/metro-runtime@55.0.6)(expo-constants@55.0.7(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.3.3))(expo-font@55.0.4(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(expo-server@55.0.6)(expo@55.0.6)(react-dom@19.2.0(react@19.2.4))(react@19.2.4)': + dependencies: + debug: 4.4.3 + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3)(utf-8-validate@5.0.10) + expo-constants: 55.0.7(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.3.3) + expo-font: 55.0.4(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + expo-server: 55.0.6 + react: 19.2.4 + optionalDependencies: + '@expo/metro-runtime': 55.0.6(@expo/dom-webview@55.0.3)(expo@55.0.6)(react-dom@19.2.0(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + react-dom: 19.2.0(react@19.2.4) + transitivePeerDependencies: + - supports-color + '@expo/schema-utils@55.0.2': {} '@expo/sdk-runtime-versions@1.0.0': {} @@ -18893,6 +19240,18 @@ snapshots: react: 19.2.0 react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + '@expo/vector-icons@15.1.1(expo-font@55.0.4(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': + dependencies: + expo-font: 55.0.4(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + + '@expo/vector-icons@15.1.1(expo-font@55.0.4(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': + dependencies: + expo-font: 55.0.4(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + react: 19.2.4 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + '@expo/ws-tunnel@1.0.6': {} '@expo/xcpretty@4.4.1': @@ -19298,11 +19657,11 @@ snapshots: '@floating-ui/core': 1.6.4 '@floating-ui/utils': 0.2.9 - '@floating-ui/react-dom@2.1.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@floating-ui/react-dom@2.1.3(react-dom@19.2.0(react@19.2.4))(react@19.2.4)': dependencies: '@floating-ui/dom': 1.6.7 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.0(react@19.2.4) '@floating-ui/utils@0.2.9': {} @@ -19319,6 +19678,13 @@ snapshots: cached-iterable: 0.3.0 react: 19.2.0 + '@fluent/react@0.15.2(@fluent/bundle@0.19.1)(react@19.2.4)': + dependencies: + '@fluent/bundle': 0.19.1 + '@fluent/sequence': 0.8.0(@fluent/bundle@0.19.1) + cached-iterable: 0.3.0 + react: 19.2.4 + '@fluent/sequence@0.8.0(@fluent/bundle@0.19.1)': dependencies: '@fluent/bundle': 0.19.1 @@ -19399,22 +19765,22 @@ snapshots: - supports-color optional: true - '@gorhom/bottom-sheet@5.2.8(@types/react@19.2.14)(react-native-gesture-handler@2.30.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native-reanimated@4.2.2(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)': + '@gorhom/bottom-sheet@5.2.8(@types/react@19.2.14)(react-native-gesture-handler@2.30.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native-reanimated@4.2.2(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': dependencies: - '@gorhom/portal': 1.0.14(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) + '@gorhom/portal': 1.0.14(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) invariant: 2.2.4 - react: 19.2.0 - react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) - react-native-gesture-handler: 2.30.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) - react-native-reanimated: 4.2.2(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-native-gesture-handler: 2.30.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + react-native-reanimated: 4.2.2(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) optionalDependencies: '@types/react': 19.2.14 - '@gorhom/portal@1.0.14(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)': + '@gorhom/portal@1.0.14(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': dependencies: nanoid: 3.3.11 - react: 19.2.0 - react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) '@grpc/grpc-js@1.10.10': dependencies: @@ -20488,264 +20854,264 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.0(react@19.2.4) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.0(react@19.2.4) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.0)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.0)': + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.0)': + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.0(react@19.2.4) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.0(react@19.2.4) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.0)': + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.0(react@19.2.4) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.0)': + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) aria-hidden: 1.2.4 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - react-remove-scroll: 2.7.1(@types/react@19.2.14)(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.0(react@19.2.4) + react-remove-scroll: 2.7.1(@types/react@19.2.14)(react@19.2.4) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@floating-ui/react-dom': 2.1.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.0) + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/react-dom': 2.1.3(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) '@radix-ui/rect': 1.1.1 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + react: 19.2.4 + react-dom: 19.2.0(react@19.2.4) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.0(react@19.2.4) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.0(react@19.2.4) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.0(react@19.2.4) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.0(react@19.2.4) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-slider@1.3.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@radix-ui/react-slider@1.3.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.0) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.0(react@19.2.4) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.0)': + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.0)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.0)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.0) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.0)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.0)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.0)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.0)': + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.0)': + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: '@radix-ui/rect': 1.1.1 - react: 19.2.0 + react: 19.2.4 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.0)': + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 optionalDependencies: '@types/react': 19.2.14 @@ -20952,6 +21318,15 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@react-native/virtualized-lists@0.83.2(@types/react@19.2.14)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + optionalDependencies: + '@types/react': 19.2.14 + '@react-native/virtualized-lists@0.83.4(@types/react@19.2.14)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': dependencies: invariant: 2.2.4 @@ -21108,77 +21483,77 @@ snapshots: '@reforged/maker-types@2.1.0': {} - '@rn-primitives/dropdown-menu@1.2.0(@rn-primitives/portal@1.3.0(@types/react@19.2.14)(immer@10.1.1)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)': + '@rn-primitives/dropdown-menu@1.2.0(@rn-primitives/portal@1.3.0(@types/react@19.2.14)(immer@10.1.1)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)))(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': dependencies: - '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@rn-primitives/hooks': 1.3.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) - '@rn-primitives/portal': 1.3.0(@types/react@19.2.14)(immer@10.1.1)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) - '@rn-primitives/slot': 1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) - '@rn-primitives/types': 1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) - '@rn-primitives/utils': 1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@rn-primitives/hooks': 1.3.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + '@rn-primitives/portal': 1.3.0(@types/react@19.2.14)(immer@10.1.1)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) + '@rn-primitives/slot': 1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + '@rn-primitives/types': 1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + '@rn-primitives/utils': 1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + react: 19.2.4 optionalDependencies: - react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) - react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) transitivePeerDependencies: - '@types/react' - '@types/react-dom' - react-dom - '@rn-primitives/hooks@1.3.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)': + '@rn-primitives/hooks@1.3.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': dependencies: - '@rn-primitives/types': 1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) - react: 19.2.0 + '@rn-primitives/types': 1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + react: 19.2.4 optionalDependencies: - react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) - react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) - '@rn-primitives/portal@1.3.0(@types/react@19.2.14)(immer@10.1.1)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0))': + '@rn-primitives/portal@1.3.0(@types/react@19.2.14)(immer@10.1.1)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))': dependencies: - react: 19.2.0 - zustand: 5.0.11(@types/react@19.2.14)(immer@10.1.1)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) + react: 19.2.4 + zustand: 5.0.11(@types/react@19.2.14)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) optionalDependencies: - react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) - react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) transitivePeerDependencies: - '@types/react' - immer - use-sync-external-store - '@rn-primitives/slider@1.2.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)': + '@rn-primitives/slider@1.2.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': dependencies: - '@radix-ui/react-slider': 1.3.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@rn-primitives/slot': 1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) - '@rn-primitives/types': 1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) - react: 19.2.0 + '@radix-ui/react-slider': 1.3.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + '@rn-primitives/slot': 1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + '@rn-primitives/types': 1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + react: 19.2.4 optionalDependencies: - react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) - react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) transitivePeerDependencies: - '@types/react' - '@types/react-dom' - react-dom - '@rn-primitives/slot@1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)': + '@rn-primitives/slot@1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: - react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) - react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) - '@rn-primitives/types@1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)': + '@rn-primitives/types@1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: - react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) - react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) - '@rn-primitives/utils@1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)': + '@rn-primitives/utils@1.2.0(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)': dependencies: - react: 19.2.0 + react: 19.2.4 optionalDependencies: - react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) - react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) '@rollup/pluginutils@5.3.0(rollup@4.59.0)': dependencies: @@ -23514,7 +23889,7 @@ snapshots: resolve-from: 5.0.0 optionalDependencies: '@babel/runtime': 7.28.6 - expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - '@babel/core' - supports-color @@ -25569,6 +25944,28 @@ snapshots: - supports-color - typescript + expo-asset@55.0.8(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3): + dependencies: + '@expo/image-utils': 0.8.12 + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) + expo-constants: 55.0.7(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.9.3) + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - supports-color + - typescript + + expo-asset@55.0.8(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3): + dependencies: + '@expo/image-utils': 0.8.12 + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3)(utf-8-validate@5.0.10) + expo-constants: 55.0.7(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.3.3) + react: 19.2.4 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - supports-color + - typescript + expo-build-properties@55.0.9(expo@55.0.6): dependencies: '@expo/schema-utils': 55.0.2 @@ -25586,6 +25983,26 @@ snapshots: - supports-color - typescript + expo-constants@55.0.7(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.9.3): + dependencies: + '@expo/config': 55.0.8(typescript@5.9.3) + '@expo/env': 2.1.1 + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - supports-color + - typescript + + expo-constants@55.0.7(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.3.3): + dependencies: + '@expo/config': 55.0.8(typescript@5.3.3) + '@expo/env': 2.1.1 + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3)(utf-8-validate@5.0.10) + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - supports-color + - typescript + expo-crypto@55.0.9(expo@55.0.6): dependencies: expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -25632,6 +26049,16 @@ snapshots: expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10) react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + expo-file-system@55.0.10(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10)): + dependencies: + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + + expo-file-system@55.0.10(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10)): + dependencies: + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3)(utf-8-validate@5.0.10) + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + expo-font@55.0.4(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0): dependencies: expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -25639,6 +26066,20 @@ snapshots: react: 19.2.0 react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + expo-font@55.0.4(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) + fontfaceobserver: 2.3.0 + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + + expo-font@55.0.4(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3)(utf-8-validate@5.0.10) + fontfaceobserver: 2.3.0 + react: 19.2.4 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + expo-image@55.0.6(expo@55.0.6)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0): dependencies: expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -25648,6 +26089,15 @@ snapshots: optionalDependencies: react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + expo-image@55.0.6(expo@55.0.6)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + sf-symbols-typescript: 2.2.0 + optionalDependencies: + react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4) + expo-json-utils@55.0.0: {} expo-keep-awake@55.0.4(expo@55.0.6)(react@19.2.0): @@ -25655,6 +26105,11 @@ snapshots: expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10) react: 19.2.0 + expo-keep-awake@55.0.4(expo@55.0.6)(react@19.2.4): + dependencies: + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) + react: 19.2.4 + expo-linking@55.0.7(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3): dependencies: expo-constants: 55.0.7(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(typescript@5.9.3) @@ -25681,6 +26136,16 @@ snapshots: - supports-color - typescript + expo-modules-autolinking@55.0.9(typescript@5.3.3): + dependencies: + '@expo/require-utils': 55.0.2(typescript@5.3.3) + '@expo/spawn-async': 1.7.2 + chalk: 4.1.2 + commander: 7.2.0 + transitivePeerDependencies: + - supports-color + - typescript + expo-modules-autolinking@55.0.9(typescript@5.9.3): dependencies: '@expo/require-utils': 55.0.2(typescript@5.9.3) @@ -25697,6 +26162,18 @@ snapshots: react: 19.2.0 react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + expo-modules-core@55.0.15(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + invariant: 2.2.4 + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + + expo-modules-core@55.0.15(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + invariant: 2.2.4 + react: 19.2.4 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + expo-notifications@55.0.12(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3): dependencies: '@expo/image-utils': 0.8.12 @@ -25716,6 +26193,11 @@ snapshots: expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10) react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + expo-screen-orientation@55.0.8(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10)): + dependencies: + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + expo-server@55.0.6: {} expo-splash-screen@55.0.10(expo@55.0.6)(typescript@5.9.3): @@ -25733,6 +26215,13 @@ snapshots: react: 19.2.0 react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + expo-sqlite@55.0.10(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + await-lock: 2.2.2 + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + expo-structured-headers@55.0.0: {} expo-system-ui@55.0.9(expo@55.0.6)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10)): @@ -25779,6 +26268,12 @@ snapshots: react: 19.2.0 react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + expo-video@55.0.10(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + expo-web-browser@15.0.10(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10)): dependencies: expo: 55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -25831,6 +26326,90 @@ snapshots: - typescript - utf-8-validate + expo@55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10): + dependencies: + '@babel/runtime': 7.28.6 + '@expo/cli': 55.0.16(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(expo-constants@55.0.7(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.9.3))(expo-font@55.0.4(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(expo@55.0.6)(react-dom@19.2.0(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@expo/config': 55.0.8(typescript@5.9.3) + '@expo/config-plugins': 55.0.6 + '@expo/devtools': 55.0.2(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + '@expo/fingerprint': 0.16.6 + '@expo/local-build-cache-provider': 55.0.6(typescript@5.9.3) + '@expo/log-box': 55.0.7(@expo/dom-webview@55.0.3)(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + '@expo/metro': 54.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@expo/metro-config': 55.0.9(bufferutil@4.0.8)(expo@55.0.6)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@expo/vector-icons': 15.1.1(expo-font@55.0.4(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + '@ungap/structured-clone': 1.3.0 + babel-preset-expo: 55.0.11(@babel/core@7.29.0)(@babel/runtime@7.28.6)(expo@55.0.6)(react-refresh@0.14.2) + expo-asset: 55.0.8(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3) + expo-constants: 55.0.7(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.9.3) + expo-file-system: 55.0.10(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10)) + expo-font: 55.0.4(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + expo-keep-awake: 55.0.4(expo@55.0.6)(react@19.2.4) + expo-modules-autolinking: 55.0.9(typescript@5.9.3) + expo-modules-core: 55.0.15(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + pretty-format: 29.7.0 + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-refresh: 0.14.2 + whatwg-url-minimum: 0.1.1 + optionalDependencies: + '@expo/dom-webview': 55.0.3(expo@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + '@expo/metro-runtime': 55.0.6(@expo/dom-webview@55.0.3)(expo@55.0.6)(react-dom@19.2.0(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + react-native-webview: 13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + transitivePeerDependencies: + - '@babel/core' + - bufferutil + - expo-router + - expo-widgets + - react-dom + - react-server-dom-webpack + - supports-color + - typescript + - utf-8-validate + + expo@55.0.6(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(react-dom@19.2.0(react@19.2.4))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3)(utf-8-validate@5.0.10): + dependencies: + '@babel/runtime': 7.28.6 + '@expo/cli': 55.0.16(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.6)(bufferutil@4.0.8)(expo-constants@55.0.7(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.3.3))(expo-font@55.0.4(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(expo@55.0.6)(react-dom@19.2.0(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3)(utf-8-validate@5.0.10) + '@expo/config': 55.0.8(typescript@5.3.3) + '@expo/config-plugins': 55.0.6 + '@expo/devtools': 55.0.2(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + '@expo/fingerprint': 0.16.6 + '@expo/local-build-cache-provider': 55.0.6(typescript@5.3.3) + '@expo/log-box': 55.0.7(@expo/dom-webview@55.0.3)(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + '@expo/metro': 54.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@expo/metro-config': 55.0.9(bufferutil@4.0.8)(expo@55.0.6)(typescript@5.3.3)(utf-8-validate@5.0.10) + '@expo/vector-icons': 15.1.1(expo-font@55.0.4(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + '@ungap/structured-clone': 1.3.0 + babel-preset-expo: 55.0.11(@babel/core@7.29.0)(@babel/runtime@7.28.6)(expo@55.0.6)(react-refresh@0.14.2) + expo-asset: 55.0.8(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.3.3) + expo-constants: 55.0.7(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.3.3) + expo-file-system: 55.0.10(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10)) + expo-font: 55.0.4(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + expo-keep-awake: 55.0.4(expo@55.0.6)(react@19.2.4) + expo-modules-autolinking: 55.0.9(typescript@5.3.3) + expo-modules-core: 55.0.15(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + pretty-format: 29.7.0 + react: 19.2.4 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-refresh: 0.14.2 + whatwg-url-minimum: 0.1.1 + optionalDependencies: + '@expo/dom-webview': 55.0.3(expo@55.0.6)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + '@expo/metro-runtime': 55.0.6(@expo/dom-webview@55.0.3)(expo@55.0.6)(react-dom@19.2.0(react@19.2.4))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + react-native-webview: 13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + transitivePeerDependencies: + - '@babel/core' + - bufferutil + - expo-router + - expo-widgets + - react-dom + - react-server-dom-webpack + - supports-color + - typescript + - utf-8-validate + exponential-backoff@3.1.1: {} exponential-backoff@3.1.3: {} @@ -27259,7 +27838,7 @@ snapshots: dependencies: '@babel/runtime': 7.28.6 - i18next-cli@1.49.7(@swc/helpers@0.5.17)(@types/node@25.5.0)(i18next@25.8.18(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(typescript@5.9.3): + i18next-cli@1.49.7(@swc/helpers@0.5.17)(@types/node@25.5.0)(i18next@25.8.18(typescript@5.9.3))(react-dom@19.2.0(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(typescript@5.9.3): dependencies: '@croct/json5-parser': 0.2.2 '@swc/core': 1.15.18(@swc/helpers@0.5.17) @@ -27275,7 +27854,7 @@ snapshots: minimatch: 10.2.4 ora: 9.3.0 react: 19.2.4 - react-i18next: 16.5.8(i18next@25.8.18(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3) + react-i18next: 16.5.8(i18next@25.8.18(typescript@5.9.3))(react-dom@19.2.0(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3) yaml: 2.8.2 transitivePeerDependencies: - '@swc/helpers' @@ -28544,6 +29123,12 @@ snapshots: react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) react-native-svg: 15.15.3(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) + lucide-react-native@0.562.0(react-native-svg@15.15.3(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-native-svg: 15.15.3(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + macos-alias@0.2.11: dependencies: nan: 2.20.0 @@ -31078,6 +31663,11 @@ snapshots: react: 19.2.0 scheduler: 0.27.0 + react-dom@19.2.0(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + react-freeze@1.0.4(react@19.2.0): dependencies: react: 19.2.0 @@ -31093,7 +31683,18 @@ snapshots: react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) typescript: 5.9.3 - react-i18next@16.5.8(i18next@25.8.18(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3): + react-i18next@15.7.4(i18next@25.8.18(typescript@5.9.3))(react-dom@19.2.0(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.28.6 + html-parse-stringify: 3.0.1 + i18next: 25.8.18(typescript@5.9.3) + react: 19.2.4 + optionalDependencies: + react-dom: 19.2.0(react@19.2.4) + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + typescript: 5.9.3 + + react-i18next@16.5.8(i18next@25.8.18(typescript@5.9.3))(react-dom@19.2.0(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4)(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.6 html-parse-stringify: 3.0.1 @@ -31101,8 +31702,8 @@ snapshots: react: 19.2.4 use-sync-external-store: 1.6.0(react@19.2.4) optionalDependencies: - react-dom: 19.2.0(react@19.2.0) - react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + react-dom: 19.2.0(react@19.2.4) + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) typescript: 5.9.3 react-is@16.13.1: {} @@ -31125,6 +31726,11 @@ snapshots: react: 19.2.0 react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + react-native-edge-to-edge@1.8.1(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-native-fit-image@1.5.5: dependencies: prop-types: 15.8.1 @@ -31137,6 +31743,14 @@ snapshots: react: 19.2.0 react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + react-native-gesture-handler@2.30.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + '@egjs/hammerjs': 2.0.17 + hoist-non-react-statics: 3.3.2 + invariant: 2.2.4 + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-native-haptic-feedback@2.3.3(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10)): dependencies: react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) @@ -31147,6 +31761,11 @@ snapshots: react: 19.2.0 react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + react-native-is-edge-to-edge@1.2.1(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-native-localize@3.7.0(@expo/config-plugins@55.0.6)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0): dependencies: react: 19.2.0 @@ -31176,11 +31795,24 @@ snapshots: react-native-worklets: 0.7.4(@babel/core@7.29.0)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) semver: 7.7.3 + react-native-reanimated@4.2.2(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4))(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-native-is-edge-to-edge: 1.2.1(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + react-native-worklets: 0.7.4(@babel/core@7.29.0)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + semver: 7.7.3 + react-native-safe-area-context@5.7.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0): dependencies: react: 19.2.0 react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + react-native-safe-area-context@5.7.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + react-native-screens@4.24.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0): dependencies: react: 19.2.0 @@ -31205,6 +31837,14 @@ snapshots: react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) warn-once: 0.1.1 + react-native-svg@15.15.3(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + css-select: 5.1.0 + css-tree: 1.1.3 + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + warn-once: 0.1.1 + react-native-url-polyfill@3.0.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10)): dependencies: react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) @@ -31225,6 +31865,22 @@ snapshots: transitivePeerDependencies: - encoding + react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.0(react@19.2.4))(react@19.2.4): + dependencies: + '@babel/runtime': 7.28.6 + '@react-native/normalize-colors': 0.74.85 + fbjs: 3.0.5(encoding@0.1.13) + inline-style-prefixer: 7.0.1 + memoize-one: 6.0.0 + nullthrows: 1.1.1 + postcss-value-parser: 4.2.0 + react: 19.2.4 + react-dom: 19.2.0(react@19.2.4) + styleq: 0.1.3 + transitivePeerDependencies: + - encoding + optional: true + react-native-webrtc@https://codeload.github.com/streamplace/react-native-webrtc/tar.gz/74fa32266e3a2fee180f5e01bb8753af2a92d9d3(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10)): dependencies: base64-js: 1.5.1 @@ -31234,6 +31890,15 @@ snapshots: transitivePeerDependencies: - supports-color + react-native-webrtc@https://codeload.github.com/streamplace/react-native-webrtc/tar.gz/74fa32266e3a2fee180f5e01bb8753af2a92d9d3(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10)): + dependencies: + base64-js: 1.5.1 + debug: 4.3.4 + event-target-shim: 6.0.2 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - supports-color + react-native-webrtc@https://codeload.github.com/streamplace/react-native-webrtc/tar.gz/74fa32266e3a2fee180f5e01bb8753af2a92d9d3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10)): dependencies: base64-js: 1.5.1 @@ -31250,6 +31915,22 @@ snapshots: react: 19.2.0 react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10) + react-native-webview@13.16.0(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + escape-string-regexp: 4.0.0 + invariant: 2.2.4 + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + optional: true + + react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + escape-string-regexp: 4.0.0 + invariant: 2.2.4 + react: 19.2.4 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + optional: true + react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0): dependencies: '@babel/core': 7.29.0 @@ -31269,6 +31950,25 @@ snapshots: transitivePeerDependencies: - supports-color + react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@babel/preset-typescript': 7.27.1(@babel/core@7.29.0) + convert-source-map: 2.0.0 + react: 19.2.4 + react-native: 0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10) + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.0)(utf-8-validate@5.0.10): dependencies: '@jest/create-cache-key-function': 29.7.0 @@ -31317,6 +32017,54 @@ snapshots: - supports-color - utf-8-validate + react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10): + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@react-native/assets-registry': 0.83.2 + '@react-native/codegen': 0.83.2(@babel/core@7.29.0) + '@react-native/community-cli-plugin': 0.83.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@react-native/gradle-plugin': 0.83.2 + '@react-native/js-polyfills': 0.83.2 + '@react-native/normalize-colors': 0.83.2 + '@react-native/virtualized-lists': 0.83.2(@types/react@19.2.14)(react-native@0.83.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10))(react@19.2.4) + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + babel-jest: 29.7.0(@babel/core@7.29.0) + babel-plugin-syntax-hermes-parser: 0.32.0 + base64-js: 1.5.1 + commander: 12.1.0 + flow-enums-runtime: 0.0.6 + glob: 7.2.3 + hermes-compiler: 0.14.1 + invariant: 2.2.4 + jest-environment-node: 29.7.0 + memoize-one: 5.2.1 + metro-runtime: 0.83.5 + metro-source-map: 0.83.5 + nullthrows: 1.1.1 + pretty-format: 29.7.0 + promise: 8.3.0 + react: 19.2.4 + react-devtools-core: 6.1.5(bufferutil@4.0.8)(utf-8-validate@5.0.10) + react-refresh: 0.14.2 + regenerator-runtime: 0.13.11 + scheduler: 0.27.0 + semver: 7.7.1 + stacktrace-parser: 0.1.10 + whatwg-fetch: 3.6.20 + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + yargs: 17.7.2 + optionalDependencies: + '@types/react': 19.2.14 + transitivePeerDependencies: + - '@babel/core' + - '@react-native-community/cli' + - '@react-native/metro-config' + - bufferutil + - supports-color + - utf-8-validate + react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.0.8)(react@19.2.4)(utf-8-validate@5.0.10): dependencies: '@jest/create-cache-key-function': 29.7.0 @@ -31369,29 +32117,29 @@ snapshots: react-refresh@0.4.3: {} - react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.0): + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): dependencies: - react: 19.2.0 - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.0) + react: 19.2.4 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 - react-remove-scroll@2.7.1(@types/react@19.2.14)(react@19.2.0): + react-remove-scroll@2.7.1(@types/react@19.2.14)(react@19.2.4): dependencies: - react: 19.2.0 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.0) - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.0) + react: 19.2.4 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.4) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.0) - use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.0) + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.4) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.4) optionalDependencies: '@types/react': 19.2.14 - react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.0): + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4): dependencies: get-nonce: 1.0.1 - react: 19.2.0 + react: 19.2.4 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 @@ -33517,9 +34265,9 @@ snapshots: url-template@3.1.1: {} - use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.0): + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4): dependencies: - react: 19.2.0 + react: 19.2.4 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 @@ -33528,10 +34276,10 @@ snapshots: dependencies: react: 19.2.0 - use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.0): + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4): dependencies: detect-node-es: 1.1.0 - react: 19.2.0 + react: 19.2.4 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 @@ -34227,4 +34975,11 @@ snapshots: react: 19.2.0 use-sync-external-store: 1.6.0(react@19.2.0) + zustand@5.0.11(@types/react@19.2.14)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): + optionalDependencies: + '@types/react': 19.2.14 + immer: 10.1.1 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + zwitch@2.0.4: {} -- 2.51.2 From c6476962dc4709f719ca96f98e8f249e5298f0e3 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Tue, 26 May 2026 18:58:55 -0700 Subject: [PATCH 08/47] vod: wire up a lot of player stuff --- js/app/components/mobile/bottom-metadata.tsx | 9 +- .../mobile/desktop-ui/top-controls.tsx | 12 +- js/app/components/mobile/ui.tsx | 17 +-- .../mobile-player/ui/viewer-context-menu.tsx | 16 +-- .../components/mobile-player/ui/viewers.tsx | 25 ++++- js/components/src/hooks/index.ts | 3 + js/components/src/hooks/useAuthor.ts | 19 ++++ js/components/src/hooks/useAvatar.ts | 18 +++ js/components/src/hooks/useViews.ts | 16 +++ pkg/api/api.go | 43 ++++++++ pkg/linking/linking.go | 104 ++++++++++++++++++ pkg/linking/linking_test.go | 42 +++++++ pkg/spxrpc/place_stream_media_getvideo.go | 6 +- pkg/spxrpc/place_stream_playback_getvideo.go | 30 +---- pkg/spxrpc/resolution_helpers.go | 37 +++++++ 15 files changed, 330 insertions(+), 67 deletions(-) create mode 100644 js/components/src/hooks/useAuthor.ts create mode 100644 js/components/src/hooks/useAvatar.ts create mode 100644 js/components/src/hooks/useViews.ts create mode 100644 pkg/spxrpc/resolution_helpers.go diff --git a/js/app/components/mobile/bottom-metadata.tsx b/js/app/components/mobile/bottom-metadata.tsx index 2ca2699ab..6f1d4e76e 100644 --- a/js/app/components/mobile/bottom-metadata.tsx +++ b/js/app/components/mobile/bottom-metadata.tsx @@ -10,9 +10,9 @@ import { PlayerUI, ShareSheet, Text, - useAvatars, + useAuthor, + useAvatar, useDID, - useLivestreamInfo, useLivestreamStore, usePlayerStore, useTheme, @@ -84,8 +84,7 @@ export function BottomMetadata({ setShowChat: (show: boolean) => void; showChat: boolean; }) { - const { profile } = useLivestreamInfo(); - const avatars = useAvatars(profile?.did ? [profile?.did] : []); + const profile = useAuthor(); const ls = useLivestreamStore((x) => x.livestream); const segment = useLivestreamStore((x) => x.segment); const mode = usePlayerStore((x) => x.mode); @@ -96,7 +95,7 @@ export function BottomMetadata({ const contentRights = segment?.contentRights; const { theme } = useTheme(); - const avatarUri = profile?.did ? avatars[profile.did]?.avatar : undefined; + const avatarUri = useAvatar(); const activity = ls?.record.activity as | ((ActivityGame | ActivityLabel) & { $type?: string }) | undefined; diff --git a/js/app/components/mobile/desktop-ui/top-controls.tsx b/js/app/components/mobile/desktop-ui/top-controls.tsx index d96b49ce6..f5ff6659a 100644 --- a/js/app/components/mobile/desktop-ui/top-controls.tsx +++ b/js/app/components/mobile/desktop-ui/top-controls.tsx @@ -4,9 +4,9 @@ import { PlayerUI, Text, View, - useAvatars, + useAuthor, + useAvatar, useCameraToggle, - useLivestreamInfo, useLivestreamStore, zero, } from "@streamplace/components"; @@ -41,9 +41,9 @@ export function TopControlBar({ embedded = false, }: TopControlBarProps) { const navigation = useNavigation(); - const { profile } = useLivestreamInfo(); + const profile = useAuthor(); const { doSetIngestCamera } = useCameraToggle(); - const avatars = useAvatars(profile?.did && embedded ? [profile?.did] : []); + const avatar = useAvatar(); const { width } = useWindowDimensions(); const isTinyScreen = width < 450; const isSmallScreen = width < 600; @@ -87,8 +87,8 @@ export function TopControlBar({ > state.mode); const muteWasForced = usePlayerStore((state) => state.muteWasForced); @@ -196,8 +195,6 @@ export function MobileUi({ > void; muteWasForced: boolean; setMuteWasForced: (forced: boolean) => void; }) { + const profile = useAuthor(); + const avatar = useAvatar(); // Get content warnings from segment const segment = useLivestreamStore((x) => x.segment); const contentWarnings = @@ -361,9 +356,7 @@ function LeftControlsPanel({ 0 ? fps : ""})` : "(Original Quality)"; - const { profile } = useLivestreamInfo(); + const profile = useAuthor(); - const avatars = useAvatars(profile?.did ? [profile?.did] : []); - const ls = useLivestreamStore((x) => x.livestream); + const avatar = useAvatar(); const segment = useLivestreamStore((x) => x.segment); const [isOpen, setIsOpen] = useState(false); @@ -159,11 +159,11 @@ export function ContextMenu({ { flex: 1, minWidth: 0 }, ]} > - {profile?.did && avatars[profile?.did]?.avatar && ( + {avatar && ( - {ls?.record.title || "Stream Title"} + {useTitle()} diff --git a/js/components/src/components/mobile-player/ui/viewers.tsx b/js/components/src/components/mobile-player/ui/viewers.tsx index aea7b81de..28f129f9b 100644 --- a/js/components/src/components/mobile-player/ui/viewers.tsx +++ b/js/components/src/components/mobile-player/ui/viewers.tsx @@ -1,15 +1,28 @@ import { Eye } from "lucide-react-native"; +import { useViews } from "../../.."; import * as atoms from "../../../lib/theme/atoms"; -import { useViewers } from "../../../livestream-store"; +import { usePlayerStore } from "../../../player-store"; import { View } from "../../ui"; import ViewerCount from "./viewer-count"; +// red reads as "live"; anything else (VOD total views) shouldn't +const LIVE_COLOR = "#fd5050"; +const VIEWS_COLOR = "#fff"; + export function Viewers() { - const viewers = useViewers(); - return ; + const views = useViews(); + const mode = usePlayerStore((x) => x.mode); + const color = mode === "live" ? LIVE_COLOR : VIEWS_COLOR; + return ; } -export function DehydratedViewers({ viewers }: { viewers: number }) { +export function DehydratedViewers({ + viewers, + color = LIVE_COLOR, +}: { + viewers: number; + color?: string; +}) { return ( - - + + ); } diff --git a/js/components/src/hooks/index.ts b/js/components/src/hooks/index.ts index b436308ad..0b54ae01f 100644 --- a/js/components/src/hooks/index.ts +++ b/js/components/src/hooks/index.ts @@ -1,5 +1,7 @@ // barrel file :) export * from "./useAQState"; +export * from "./useAuthor"; +export * from "./useAvatar"; export * from "./useAvatars"; export * from "./useCameraToggle"; export * from "./useDocumentTitle"; @@ -12,3 +14,4 @@ export * from "./usePointerDevice"; export * from "./useSegmentDimensions"; export * from "./useSegmentTiming"; export * from "./useTitle"; +export * from "./useViews"; diff --git a/js/components/src/hooks/useAuthor.ts b/js/components/src/hooks/useAuthor.ts new file mode 100644 index 000000000..c5b96df91 --- /dev/null +++ b/js/components/src/hooks/useAuthor.ts @@ -0,0 +1,19 @@ +import { AppBskyActorDefs } from "@atproto/api"; +import { useLivestreamStoreOptional } from "../livestream-store"; +import { usePlayerStore } from "../player-store"; +import { useVideoStoreOptional } from "../video-store"; + +// Surfaces the profile of whoever created what the player is currently +// showing — the streamer in live mode, the uploader in vod mode — switching +// on the player's mode. Like useTitle, this keeps player UI generic: +// `const author = useAuthor()` works the same in live and vod contexts. +// +// Reads from the *optional* store hooks so it's safe to call even when only +// one of the providers (LivestreamProvider / VideoProvider) is mounted. +// Returns null until the relevant metadata has loaded. +export function useAuthor(): AppBskyActorDefs.ProfileViewBasic | null { + const mode = usePlayerStore((x) => x.mode); + const liveProfile = useLivestreamStoreOptional((x) => x.profile); + const vodAuthor = useVideoStoreOptional((x) => x.video?.author ?? null); + return mode === "vod" ? vodAuthor : liveProfile; +} diff --git a/js/components/src/hooks/useAvatar.ts b/js/components/src/hooks/useAvatar.ts new file mode 100644 index 000000000..daa6cb88b --- /dev/null +++ b/js/components/src/hooks/useAvatar.ts @@ -0,0 +1,18 @@ +import { useMemo } from "react"; +import { useAuthor } from "./useAuthor"; +import { useAvatars } from "./useAvatars"; + +// Surfaces the author's avatar URL for whatever the player is currently +// showing, switching on the player's mode via useAuthor. In both modes we +// prefer the hydrated ProfileViewDetailed avatar (fetched through the profile +// cache) and fall back to whatever the basic profile/author view carried. +// Returns undefined when no avatar is known yet. +export function useAvatar(): string | undefined { + const author = useAuthor(); + const did = author?.did; + // memoized so useAvatars' effect doesn't refire on every render + const dids = useMemo(() => (did ? [did] : []), [did]); + const detailed = useAvatars(dids); + if (!did) return undefined; + return detailed[did]?.avatar ?? author?.avatar; +} diff --git a/js/components/src/hooks/useViews.ts b/js/components/src/hooks/useViews.ts new file mode 100644 index 000000000..d6d22d086 --- /dev/null +++ b/js/components/src/hooks/useViews.ts @@ -0,0 +1,16 @@ +import { useLivestreamStoreOptional } from "../livestream-store"; +import { usePlayerStore } from "../player-store"; +import { useVideoStoreOptional } from "../video-store"; + +// Surfaces the view count for whatever the player is currently showing, +// switching on the player's mode: the live concurrent viewer count in live +// mode, or the VOD's total accumulated view count (from VideoView.viewCounts) +// in vod mode. Returns null when the count isn't known yet. +export function useViews(): number | null { + const mode = usePlayerStore((x) => x.mode); + const liveViewers = useLivestreamStoreOptional((x) => x.viewers); + const vodViews = useVideoStoreOptional( + (x) => x.video?.viewCounts?.count ?? null, + ); + return mode === "vod" ? vodViews : liveViewers; +} diff --git a/pkg/api/api.go b/pkg/api/api.go index 80ce35982..bd43ab1ce 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -387,6 +387,18 @@ func (a *StreamplaceAPI) NotFoundLinkingHandler(ctx context.Context, linker *lin } req.URL.Host = req.Host req.URL.Scheme = proto + + // VOD link cards live at //video/. Everything else with a + // slash in it falls through to static-file / default-card handling. + parts := strings.Split(strings.Trim(req.URL.Path, "/"), "/") + if len(parts) == 3 && parts[1] == "video" { + if a.writeVideoCard(ctx, w, req, linker, parts[0], parts[2]) { + return + } + defaultHandler.ServeHTTP(w, req) + return + } + maybeHandle := strings.TrimPrefix(req.URL.Path, "/") // quick check for things that aren't valid handles/dids if strings.ContainsAny(maybeHandle, "/_") { @@ -424,6 +436,37 @@ func (a *StreamplaceAPI) NotFoundLinkingHandler(ctx context.Context, linker *lin }), nil } +// writeVideoCard renders an OpenGraph link card for a VOD at +// //video/, pulling the video out of the local index. It returns +// true when it wrote a response, and false (having written nothing) when the +// video can't be found or rendered, so the caller can fall back to +// static-file / default-card handling. +func (a *StreamplaceAPI) writeVideoCard(ctx context.Context, w http.ResponseWriter, req *http.Request, linker *linking.Linker, user, tid string) bool { + repo, err := a.Model.GetRepoByHandleOrDID(user) + if err != nil || repo == nil { + return false + } + uri := fmt.Sprintf("at://%s/place.stream.video/%s", repo.DID, tid) + vv, err := a.Model.GetVideoView(ctx, uri) + if err != nil { + log.Error(ctx, "error fetching video view for card", "uri", uri, "error", err) + return false + } + if vv == nil { + return false + } + bs, err := linker.GenerateVideoCard(ctx, req.URL, vv, a.CLI.SentryDSN) + if err != nil { + log.Error(ctx, "error generating video card", "uri", uri, "error", err) + return false + } + w.Header().Set("Content-Type", "text/html") + if _, err := w.Write(bs); err != nil { + log.Error(ctx, "error writing response", "error", err) + } + return true +} + func (a *StreamplaceAPI) MistProxyHandler(ctx context.Context, tmpl string) httprouter.Handle { return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { if !a.CLI.HasMist() { diff --git a/pkg/linking/linking.go b/pkg/linking/linking.go index b89dd9f66..748998383 100644 --- a/pkg/linking/linking.go +++ b/pkg/linking/linking.go @@ -176,6 +176,110 @@ func (l *Linker) GenerateStreamerCard(ctx context.Context, u *url.URL, lsv *stre }) } +func (l *Linker) GenerateVideoCard(ctx context.Context, u *url.URL, vv *streamplace.MediaGetVideo_VideoView, sentryDSN string) ([]byte, error) { + if u == nil { + return nil, errors.New("url is nil") + } + if vv == nil { + return nil, errors.New("video view is nil") + } + video, ok := vv.Record.Val.(*streamplace.Video) + if !ok { + return nil, errors.New("video view record is not a video") + } + + outURL := u.String() + + authorDid := "" + authorHandle := "" + if vv.Author != nil { + authorDid = vv.Author.Did + authorHandle = vv.Author.Handle + } + + // og:image is the VOD's own thumbnail, served through the Bluesky image + // CDN (which fetches the blob from the author's PDS — the same trick used + // for game cover art). When the video has no thumbnail we fall back to the + // author's generated profile card so the link still carries an image. + var imageURL string + if video.Thumb != nil && authorDid != "" { + imageURL = fmt.Sprintf( + "https://cdn.bsky.app/img/feed_thumbnail/plain/%s/%s@jpeg", + authorDid, video.Thumb.Ref.String(), + ) + } else { + cardURL, _ := url.Parse(u.String()) + cardURL.Path = "/xrpc/place.stream.live.getProfileCard" + cardURL.RawQuery = fmt.Sprintf("id=%s", authorDid) + imageURL = cardURL.String() + } + + titleStr := fmt.Sprintf("@%s's video on ", authorHandle) + + // Define all meta tags + metaTags := []MetaTag{ + // Basic meta + {Type: "name", Key: "description", Content: video.Title}, + + // Facebook Meta Tags + {Type: "property", Key: "og:url", Content: outURL}, + {Type: "property", Key: "og:type", Content: "video.other"}, + {Type: "property", Key: "og:description", Content: video.Title}, + {Type: "property", Key: "og:image", Content: imageURL}, + + // Twitter Meta Tags + {Type: "name", Key: "twitter:card", Content: "summary_large_image"}, + {Type: "property", Key: "twitter:domain", Content: u.Host}, + {Type: "property", Key: "twitter:url", Content: outURL}, + {Type: "name", Key: "twitter:description", Content: video.Title}, + {Type: "name", Key: "twitter:image", Content: imageURL}, + } + + brandingTitle := "streamplace node" + if l.sdb != nil && l.cli != nil { + branding, err := l.getBrandingAssets("did:web:" + l.cli.BroadcasterHost) + if err == nil { + for i := range branding { + val := branding[i] + if val.Key == "siteTitle" && val.Data != nil { + brandingTitle = *val.Data + } + marshalledJson, err := json.Marshal(val) + if err != nil { + fmt.Printf("error marshalling branding asset %s: %v\n", val.Key, err) + continue + } + metaTags = append(metaTags, MetaTag{ + Type: "name", + Key: "internal-brand:" + val.Key, + Content: string(marshalledJson), + }) + } + } else { + // log but we should not block rendering + fmt.Printf("error fetching branding assets: %v\n", err) + } + } + + // do twitter/og title after + metaTags = append(metaTags, MetaTag{ + Type: "property", + Key: "og:title", + Content: fmt.Sprintf("%s%s", titleStr, brandingTitle), + }) + metaTags = append(metaTags, MetaTag{ + Type: "name", + Key: "twitter:title", + Content: fmt.Sprintf("%s%s", titleStr, brandingTitle), + }) + + return l.GenerateHTML(ctx, &PageConfig{ + Title: fmt.Sprintf("%s%s", titleStr, brandingTitle), + Metas: metaTags, + SentryDSN: sentryDSN, + }) +} + func (l *Linker) GenerateDefaultCard(ctx context.Context, u *url.URL, sentryDSN string) ([]byte, error) { if u == nil { return nil, errors.New("url is nil") diff --git a/pkg/linking/linking_test.go b/pkg/linking/linking_test.go index d442f7920..e6b23de5e 100644 --- a/pkg/linking/linking_test.go +++ b/pkg/linking/linking_test.go @@ -10,6 +10,7 @@ import ( "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/api/bsky" lexutil "github.com/bluesky-social/indigo/lex/util" + "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" "stream.place/streamplace/js/app" "stream.place/streamplace/pkg/streamplace" @@ -69,3 +70,44 @@ func TestGenerateLinkCard(t *testing.T) { require.True(t, strings.Contains(linkStr, ls.Title), "should contain the livestream title") require.True(t, strings.Count(linkStr, "") == 1, "should have exactly one title tag") } + +func TestGenerateVideoCard(t *testing.T) { + index := IndexHTML(t) + linker, err := NewLinker(context.Background(), index, nil, nil) + require.NoError(t, err) + require.NotNil(t, linker) + + u, err := url.Parse("https://stream.place/iame.li/video/3mms3tfkcxstu") + require.NoError(t, err) + + thumbCID := "bafyreib2ohz45jileumnuwa3wdoo3o7caikfyq467eanleqcscouh5wery" + c, err := cid.Decode(thumbCID) + require.NoError(t, err) + + video := &streamplace.Video{ + Title: "My excellent VOD", + Thumb: &lexutil.LexBlob{ + Ref: lexutil.LexLink(c), + MimeType: "image/jpeg", + }, + } + vv := &streamplace.MediaGetVideo_VideoView{ + Author: &bsky.ActorDefs_ProfileViewBasic{ + Handle: "iame.li", + Did: "did:plc:2zmxikig2sj7gqaezl5gntae", + }, + Cid: "bafyreib2ohz45jileumnuwa3wdoo3o7caikfyq467eanleqcscouh5wery", + Record: &lexutil.LexiconTypeDecoder{Val: video}, + Uri: "at://did:plc:2zmxikig2sj7gqaezl5gntae/place.stream.video/3mms3tfkcxstu", + } + + linkCard, err := linker.GenerateVideoCard(context.Background(), u, vv, "") + require.NoError(t, err) + linkStr := string(linkCard) + require.True(t, strings.Contains(linkStr, "iame.li"), "should contain the author handle") + require.True(t, strings.Contains(linkStr, video.Title), "should contain the video title") + require.True(t, strings.Contains(linkStr, + "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:2zmxikig2sj7gqaezl5gntae/"+thumbCID+"@jpeg"), + "og:image should be the video thumbnail served via the bsky CDN") + require.True(t, strings.Count(linkStr, "<title>") == 1, "should have exactly one title tag") +} diff --git a/pkg/spxrpc/place_stream_media_getvideo.go b/pkg/spxrpc/place_stream_media_getvideo.go index ea7eaf2e7..0dd430388 100644 --- a/pkg/spxrpc/place_stream_media_getvideo.go +++ b/pkg/spxrpc/place_stream_media_getvideo.go @@ -4,7 +4,6 @@ import ( "context" "net/http" - "github.com/bluesky-social/indigo/atproto/syntax" "github.com/labstack/echo/v4" placestream "stream.place/streamplace/pkg/streamplace" @@ -20,10 +19,11 @@ func (s *Server) handlePlaceStreamMediaGetVideo(ctx context.Context, uri string) if uri == "" { return nil, echo.NewHTTPError(http.StatusBadRequest, "uri is required") } - if _, err := syntax.ParseATURI(uri); err != nil { + aturi, err := s.normalizeURI(ctx, uri) + if err != nil { return nil, echo.NewHTTPError(http.StatusBadRequest, "uri must be a valid AT-URI: "+err.Error()) } - view, err := s.model.GetVideoView(ctx, uri) + view, err := s.model.GetVideoView(ctx, aturi.String()) if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } diff --git a/pkg/spxrpc/place_stream_playback_getvideo.go b/pkg/spxrpc/place_stream_playback_getvideo.go index 881abd1fc..8a20789bd 100644 --- a/pkg/spxrpc/place_stream_playback_getvideo.go +++ b/pkg/spxrpc/place_stream_playback_getvideo.go @@ -265,7 +265,7 @@ func (s *Server) HandleGetVideoPlaylist(c echo.Context) error { if rawURI == "" { return echo.NewHTTPError(http.StatusBadRequest, "uri is required") } - aturi, err := syntax.ParseATURI(rawURI) + aturi, err := s.normalizeURI(ctx, rawURI) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "uri must be a valid AT-URI: "+err.Error()) } @@ -273,30 +273,6 @@ func (s *Server) HandleGetVideoPlaylist(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("UnsupportedCollection: %s", aturi.Collection())) } - var ownerDID syntax.DID - if aturi.Authority().IsHandle() { - handle, err := aturi.Authority().AsHandle() - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "failed to convert handle: "+err.Error()) - } - res, err := s.resolveStreamer(ctx, handle.String()) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "failed to resolve streamer: "+err.Error()) - } - ownerDID, err = syntax.ParseDID(res) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "resolved streamer must be a valid DID: "+err.Error()) - } - aturi, err = syntax.ParseATURI(fmt.Sprintf("at://%s/%s", ownerDID.String(), aturi.Path())) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "failed to parse AT-URI: "+err.Error()) - } - } else { - ownerDID, err = aturi.Authority().AsDID() - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "uri authority must be a DID: "+err.Error()) - } - } if s.playbackStore == nil { return echo.NewHTTPError(http.StatusServiceUnavailable, "playback store not configured") } @@ -334,7 +310,7 @@ func (s *Server) HandleGetVideoPlaylist(c echo.Context) error { } else if labeled { return echo.NewHTTPError(http.StatusForbidden, "VideoUnavailable") } - if banned, err := s.accountBanned(ownerDID.String()); err != nil { + if banned, err := s.accountBanned(aturi.Authority().String()); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } else if banned { return echo.NewHTTPError(http.StatusForbidden, "VideoUnavailable") @@ -365,7 +341,7 @@ func (s *Server) HandleGetVideoPlaylist(c echo.Context) error { // server side, so the propagation stays in clip-local time. body = masterPlaylist(meta, uri, sid, startMS, endMS) } else { - body, err = mediaPlaylist(meta, track, ownerDID.String(), sid, s.cli.VODCDNURL, effectiveStartMS, effectiveEndMS) + body, err = mediaPlaylist(meta, track, aturi.Authority().String(), sid, s.cli.VODCDNURL, effectiveStartMS, effectiveEndMS) if err != nil { return err } diff --git a/pkg/spxrpc/resolution_helpers.go b/pkg/spxrpc/resolution_helpers.go new file mode 100644 index 000000000..c52cc6b88 --- /dev/null +++ b/pkg/spxrpc/resolution_helpers.go @@ -0,0 +1,37 @@ +package spxrpc + +import ( + "context" + "fmt" + "net/http" + + "github.com/bluesky-social/indigo/atproto/syntax" + "github.com/labstack/echo/v4" +) + +// takes in and returns an aturi, doing handle resolution if necessary on the way +func (s *Server) normalizeURI(ctx context.Context, uri string) (*syntax.ATURI, error) { + aturi, err := syntax.ParseATURI(uri) + if err != nil { + return nil, echo.NewHTTPError(http.StatusBadRequest, "failed to parse AT-URI: "+err.Error()) + } + + if aturi.Authority().IsHandle() { + handle, err := aturi.Authority().AsHandle() + if err != nil { + return nil, echo.NewHTTPError(http.StatusBadRequest, "failed to convert handle: "+err.Error()) + } + res, err := s.resolveStreamer(ctx, handle.String()) + if err != nil { + return nil, echo.NewHTTPError(http.StatusBadRequest, "failed to resolve streamer: "+err.Error()) + } + aturi, err = syntax.ParseATURI(fmt.Sprintf("at://%s/%s", res, aturi.Path())) + if err != nil { + return nil, echo.NewHTTPError(http.StatusBadRequest, "failed to parse AT-URI: "+err.Error()) + } + } else if !aturi.Authority().IsDID() { + return nil, echo.NewHTTPError(http.StatusBadRequest, "uri authority must be a DID or handle: "+uri) + } + + return &aturi, nil +} -- 2.51.2 From 7d82197645c3dc3c14950ca1d3eb29c708bef6bf Mon Sep 17 00:00:00 2001 From: Eli Mallon <eli@iame.li> Date: Tue, 26 May 2026 20:24:58 -0700 Subject: [PATCH 09/47] player: sort of kind of working on mobile! og cards! --- go.mod | 8 +-- go.sum | 16 +++--- js/app/components/mobile/ui.tsx | 18 +++++-- .../mobile-player/video-async.native.tsx | 2 +- .../src/hooks/useSegmentDimensions.tsx | 54 +++++++++++++++---- .../media/place-stream-media-getvideo.md | 8 +++ .../media/place-stream-media-track.md | 38 +++++++++++++ .../content/docs/lex-reference/openapi.json | 24 +++++++++ js/streamplace/src/useful-types.ts | 6 +++ lexicons/place/stream/media/getVideo.json | 7 +++ lexicons/place/stream/media/track.json | 13 +++++ pkg/linking/linking.go | 36 ++++++------- pkg/linking/linking_test.go | 5 +- pkg/model/video.go | 24 +++++++++ pkg/streamplace/mediagetVideo.go | 1 + pkg/streamplace/mediatrack.go | 9 ++++ 16 files changed, 220 insertions(+), 49 deletions(-) diff --git a/go.mod b/go.mod index 147824dd6..d98b3625b 100644 --- a/go.mod +++ b/go.mod @@ -63,9 +63,9 @@ require ( github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/slok/go-http-metrics v0.13.0 github.com/starttoaster/prometheus-exporter-scraper v0.0.1 - github.com/streamplace/atproto-oauth-golang v0.0.0-20260413212710-98956064d06c - github.com/streamplace/muxl/go v0.0.0-20260528173527-91f80c95ca45 - github.com/streamplace/oatproxy v0.0.0-20260602023256-6c7e423d20a0 + github.com/streamplace/atproto-oauth-golang v0.0.0-20250619231223-a9c04fb888ac + github.com/streamplace/muxl/go v0.2.1 + github.com/streamplace/oatproxy v0.0.0-20260508220721-f8852e8dbf44 github.com/stretchr/testify v1.11.1 github.com/tdewolff/canvas v0.0.0-20250728095813-50d4cb1eee71 github.com/tus/tusd/v2 v2.8.0 @@ -496,7 +496,7 @@ require ( github.com/tdewolff/minify/v2 v2.23.4 // indirect github.com/tdewolff/parse/v2 v2.8.0 // indirect github.com/tetafro/godot v1.5.1 // indirect - github.com/tetratelabs/wazero v1.11.0 // indirect + github.com/tetratelabs/wazero v1.12.0 // indirect github.com/thales-e-security/pool v0.0.2 // indirect github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect github.com/timonwong/loggercheck v0.11.0 // indirect diff --git a/go.sum b/go.sum index 29464487e..05aa64ea6 100644 --- a/go.sum +++ b/go.sum @@ -1369,16 +1369,16 @@ github.com/starttoaster/prometheus-exporter-scraper v0.0.1 h1:E3Zl3ho3v3Yt2Yk7i9 github.com/starttoaster/prometheus-exporter-scraper v0.0.1/go.mod h1:qNccVOSo0np2jYi6DolRyUICFcMkhH5k1Gs7CYxav1w= github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4= github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk= -github.com/streamplace/atproto-oauth-golang v0.0.0-20260413212710-98956064d06c h1:IzEPU2O4iL58Nb7aw+7lB9ttnesEwOVVE5oV9NEXemM= -github.com/streamplace/atproto-oauth-golang v0.0.0-20260413212710-98956064d06c/go.mod h1:9LlKkqciiO5lRfbX0n4Wn5KNY9nvFb4R3by8FdW2TWc= +github.com/streamplace/atproto-oauth-golang v0.0.0-20250619231223-a9c04fb888ac h1:heVM4CGox3kfJclSmagsI3hvKk7W52EegnFqqs9CSYk= +github.com/streamplace/atproto-oauth-golang v0.0.0-20250619231223-a9c04fb888ac/go.mod h1:9LlKkqciiO5lRfbX0n4Wn5KNY9nvFb4R3by8FdW2TWc= github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4 h1:L1fS4HJSaAyNnkwfuZubgfeZy8rkWmA0cMtH5Z0HqNc= github.com/streamplace/go-dpop v0.0.0-20250510031900-c897158a8ad4/go.mod h1:bGUXY9Wd4mnd+XUrOYZr358J2f6z9QO/dLhL1SsiD+0= github.com/streamplace/indigo v0.0.0-20260218231908-939cdaf0c507 h1:e8M3qPLr37NxEjlr18TaAwGP+OVyherVjgUG5VVmgWI= github.com/streamplace/indigo v0.0.0-20260218231908-939cdaf0c507/go.mod h1:Pm2I1+iDXn/hLbF7XCg/DsZi6uDCiOo7hZGWprSM7k0= -github.com/streamplace/muxl/go v0.0.0-20260528173527-91f80c95ca45 h1:v9KQ+tQnKwbSvS5orbszrd49ucySYi4F9npxoU2sLFU= -github.com/streamplace/muxl/go v0.0.0-20260528173527-91f80c95ca45/go.mod h1:aCyYTW3o6c1Kush9UJ/Yv6EYMUbj8l8GTD7cHKcSxw8= -github.com/streamplace/oatproxy v0.0.0-20260602023256-6c7e423d20a0 h1:FtJV63T621Dg5rQh049uTCx6kGQiiAXCReICz3hJSRQ= -github.com/streamplace/oatproxy v0.0.0-20260602023256-6c7e423d20a0/go.mod h1:wpY+T/wE00jrUhgh2dKXbbE91D36u86KGlENK/hWFkE= +github.com/streamplace/muxl/go v0.2.1 h1:ITjmjP3OREBgLowafspw7R5kGjmKX3uE6/ATAUIAHPk= +github.com/streamplace/muxl/go v0.2.1/go.mod h1:aCyYTW3o6c1Kush9UJ/Yv6EYMUbj8l8GTD7cHKcSxw8= +github.com/streamplace/oatproxy v0.0.0-20260508220721-f8852e8dbf44 h1:b38ToXNQCvKqBlx0SeQYUOhx6uqqnrF5AL6B0Z3zzdk= +github.com/streamplace/oatproxy v0.0.0-20260508220721-f8852e8dbf44/go.mod h1:j1+zdhe1IC0+PTE2rIXk2LqYn4Lz2x9SJaV3eHkQyfs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -1424,8 +1424,8 @@ github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpR github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= github.com/tetafro/godot v1.5.1 h1:PZnjCol4+FqaEzvZg5+O8IY2P3hfY9JzRBNPv1pEDS4= github.com/tetafro/godot v1.5.1/go.mod h1:cCdPtEndkmqqrhiCfkmxDodMQJ/f3L1BCNskCUZdTwk= -github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= -github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= +github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk= diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index a9c25be03..2c40879cb 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -160,7 +160,18 @@ export function MobileUi({ const hover = Gesture.Hover().onChange(onPlayerHover); const pan = Gesture.Pan().onChange(onPlayerHover); const tap = Gesture.Tap().onEnd(onPlayerHover); - + let chatSection: React.ReactNode = null; + if ( + mode !== "vod" && + !hideMobileChat && + !isSelfAndNotLive && + playerIsReady && + isPortrait + ) { + chatSection = ( + <MobileChatPanel isPlayerRatioGreater={isPlayerRatioGreater} /> + ); + } const combined = Gesture.Race(hover, pan, tap); return ( <> @@ -301,10 +312,7 @@ export function MobileUi({ <PlayerUI.AutoplayButton /> </View> </GestureDetector> - {hideMobileChat || - (isPortrait && !isSelfAndNotLive && playerIsReady && ( - <MobileChatPanel isPlayerRatioGreater={isPlayerRatioGreater} /> - ))} + {chatSection} </> ); } diff --git a/js/components/src/components/mobile-player/video-async.native.tsx b/js/components/src/components/mobile-player/video-async.native.tsx index 8ad93783c..da0700f6b 100644 --- a/js/components/src/components/mobile-player/video-async.native.tsx +++ b/js/components/src/components/mobile-player/video-async.native.tsx @@ -223,7 +223,7 @@ export function NativeVideo(props?: { <VideoView ref={videoRef} player={player} - nativeControls={fullscreen} + nativeControls={fullscreen || mode === "vod"} onFullscreenEnter={() => { setFullscreen(true); }} diff --git a/js/components/src/hooks/useSegmentDimensions.tsx b/js/components/src/hooks/useSegmentDimensions.tsx index bb95718e1..f61b68357 100644 --- a/js/components/src/hooks/useSegmentDimensions.tsx +++ b/js/components/src/hooks/useSegmentDimensions.tsx @@ -1,18 +1,52 @@ -import { useLivestreamStore } from "../livestream-store"; +import { + PlaceStreamMediaTrack, + PlaceStreamSegment, + VideoViewHydrated, +} from "streamplace"; +import { useLivestreamStoreOptional } from "../livestream-store"; +import { useVideoStoreOptional } from "../video-store"; export function useSegmentDimensions() { - const latestSegment = useLivestreamStore((x) => x.segment); + const video = useVideoStoreOptional((x) => x.video); + const latestSegment = useLivestreamStoreOptional((x) => x.segment); - let seg = latestSegment?.video && latestSegment.video[0]; + let height = 0; + let width = 0; + if (video) { + const meta = getVideoMetadata(video); + if (meta) { + height = meta.height || 0; + width = meta.width || 0; + } + } else { + let seg = latestSegment?.video && latestSegment.video[0]; - let ratio = { - height: seg?.height || 0, - width: seg?.width || 0, - }; + height = seg?.height || 0; + width = seg?.width || 0; + } return { - isPlayerRatioGreater: ratio.width > ratio.height, - height: ratio.height, - width: ratio.width, + isPlayerRatioGreater: width > height, + height: height, + width: width, }; } + +function getVideoMetadata( + view: VideoViewHydrated, +): PlaceStreamSegment.Video | null { + if (!view.tracks) { + return null; + } + for (const track of view.tracks) { + const meta = track.record.metadata; + if (!PlaceStreamMediaTrack.isCommonMetadata(meta)) { + continue; + } + if (!meta.video) { + continue; + } + return meta.video; + } + return null; +} diff --git a/js/docs/src/content/docs/lex-reference/media/place-stream-media-getvideo.md b/js/docs/src/content/docs/lex-reference/media/place-stream-media-getvideo.md index ee6c54c8c..ecbd9f611 100644 --- a/js/docs/src/content/docs/lex-reference/media/place-stream-media-getvideo.md +++ b/js/docs/src/content/docs/lex-reference/media/place-stream-media-getvideo.md @@ -49,6 +49,7 @@ Get a hydrated view of a place.stream.video record — the record itself plus au | `author` | [`app.bsky.actor.defs#profileViewBasic`](https://github.com/bluesky-social/atproto/tree/main/lexicons/app/bsky/actor/defs.json#profileViewBasic) | ✅ | | | | `record` | `unknown` | ✅ | | | | `viewCounts` | [`#viewCountSummary`](#viewcountsummary) | ✅ | Aggregated view counts across every indexed reporter. Always present; zero-valued (count=0, reporters=0) when no place.stream.media.viewCount records have been observed yet, so consumers can render a count unconditionally. | | +| `tracks` | Array of [`place.stream.media.track#trackView`](/lex-reference/place-stream-media-track#trackview) | ❌ | | | --- @@ -129,6 +130,13 @@ Sums across every place.stream.media.viewCount record indexed for this video, re "type": "ref", "ref": "#viewCountSummary", "description": "Aggregated view counts across every indexed reporter. Always present; zero-valued (count=0, reporters=0) when no place.stream.media.viewCount records have been observed yet, so consumers can render a count unconditionally." + }, + "tracks": { + "type": "array", + "items": { + "type": "ref", + "ref": "place.stream.media.track#trackView" + } } } }, diff --git a/js/docs/src/content/docs/lex-reference/media/place-stream-media-track.md b/js/docs/src/content/docs/lex-reference/media/place-stream-media-track.md index 74f8bafee..b948bd80a 100644 --- a/js/docs/src/content/docs/lex-reference/media/place-stream-media-track.md +++ b/js/docs/src/content/docs/lex-reference/media/place-stream-media-track.md @@ -47,6 +47,23 @@ Metadata common to all media types. Contains subobjects for other media types. --- +<a name="trackview"></a> + +### `trackView` + +**Type:** `object` + +**Properties:** + +| Name | Type | Req'd | Description | Constraints | +| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | ----------- | ---------------- | +| `uri` | `string` | ✅ | | Format: `at-uri` | +| `cid` | `string` | ✅ | | Format: `cid` | +| `author` | [`app.bsky.actor.defs#profileViewBasic`](https://github.com/bluesky-social/atproto/tree/main/lexicons/app/bsky/actor/defs.json#profileViewBasic) | ✅ | | | +| `record` | `unknown` | ✅ | | | + +--- + ## Lexicon Source ```json @@ -105,6 +122,27 @@ Metadata common to all media types. Contains subobjects for other media types. "ref": "place.stream.segment#audio" } } + }, + "trackView": { + "type": "object", + "required": ["uri", "cid", "author", "record"], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "author": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileViewBasic" + }, + "record": { + "type": "unknown" + } + } } } } diff --git a/js/docs/src/content/docs/lex-reference/openapi.json b/js/docs/src/content/docs/lex-reference/openapi.json index ef53b6590..9a5e5efde 100644 --- a/js/docs/src/content/docs/lex-reference/openapi.json +++ b/js/docs/src/content/docs/lex-reference/openapi.json @@ -4732,6 +4732,12 @@ "viewCounts": { "$ref": "#/components/schemas/place.stream.media.getVideo_viewCountSummary", "description": "Aggregated view counts across every indexed reporter. Always present; zero-valued (count=0, reporters=0) when no place.stream.media.viewCount records have been observed yet, so consumers can render a count unconditionally." + }, + "tracks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/place.stream.media.track_trackView" + } } }, "required": ["uri", "cid", "author", "record", "viewCounts"] @@ -5100,6 +5106,24 @@ }, "required": ["count", "bytes", "durationMs", "reporters"] }, + "place.stream.media.track_trackView": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "format": "uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "author": { + "$ref": "#/components/schemas/app.bsky.actor.defs_profileViewBasic" + }, + "record": {} + }, + "required": ["uri", "cid", "author", "record"] + }, "place.stream.video": { "description": "Unknown type" }, diff --git a/js/streamplace/src/useful-types.ts b/js/streamplace/src/useful-types.ts index 230bf200d..263d4f8ea 100644 --- a/js/streamplace/src/useful-types.ts +++ b/js/streamplace/src/useful-types.ts @@ -4,6 +4,7 @@ import { PlaceStreamChatPinnedRecord, PlaceStreamLivestream, PlaceStreamMediaGetVideo, + PlaceStreamMediaTrack, } from "./lexicons"; export interface LivestreamViewHydrated @@ -23,4 +24,9 @@ export interface PinnedRecordViewHydrated export interface VideoViewHydrated extends PlaceStreamMediaGetVideo.VideoView { record: PlaceStreamLivestream.Record; + tracks: TrackViewHydrated[]; +} + +export interface TrackViewHydrated extends PlaceStreamMediaTrack.TrackView { + record: PlaceStreamMediaTrack.Record; } diff --git a/lexicons/place/stream/media/getVideo.json b/lexicons/place/stream/media/getVideo.json index d210cd528..86f5ae1eb 100644 --- a/lexicons/place/stream/media/getVideo.json +++ b/lexicons/place/stream/media/getVideo.json @@ -45,6 +45,13 @@ "type": "ref", "ref": "#viewCountSummary", "description": "Aggregated view counts across every indexed reporter. Always present; zero-valued (count=0, reporters=0) when no place.stream.media.viewCount records have been observed yet, so consumers can render a count unconditionally." + }, + "tracks": { + "type": "array", + "items": { + "type": "ref", + "ref": "place.stream.media.track#trackView" + } } } }, diff --git a/lexicons/place/stream/media/track.json b/lexicons/place/stream/media/track.json index e2ea1fe3a..8e0af8ab2 100644 --- a/lexicons/place/stream/media/track.json +++ b/lexicons/place/stream/media/track.json @@ -53,6 +53,19 @@ "ref": "place.stream.segment#audio" } } + }, + "trackView": { + "type": "object", + "required": ["uri", "cid", "author", "record"], + "properties": { + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "author": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileViewBasic" + }, + "record": { "type": "unknown" } + } } } } diff --git a/pkg/linking/linking.go b/pkg/linking/linking.go index 748998383..aafda0cbc 100644 --- a/pkg/linking/linking.go +++ b/pkg/linking/linking.go @@ -214,24 +214,19 @@ func (l *Linker) GenerateVideoCard(ctx context.Context, u *url.URL, vv *streampl imageURL = cardURL.String() } - titleStr := fmt.Sprintf("@%s's video on ", authorHandle) - - // Define all meta tags + // Define all meta tags. The title/description pair is appended after we + // resolve the node's branding title below: the card headline is the + // video's own title, and the author + node go in the description. metaTags := []MetaTag{ - // Basic meta - {Type: "name", Key: "description", Content: video.Title}, - // Facebook Meta Tags {Type: "property", Key: "og:url", Content: outURL}, {Type: "property", Key: "og:type", Content: "video.other"}, - {Type: "property", Key: "og:description", Content: video.Title}, {Type: "property", Key: "og:image", Content: imageURL}, // Twitter Meta Tags {Type: "name", Key: "twitter:card", Content: "summary_large_image"}, {Type: "property", Key: "twitter:domain", Content: u.Host}, {Type: "property", Key: "twitter:url", Content: outURL}, - {Type: "name", Key: "twitter:description", Content: video.Title}, {Type: "name", Key: "twitter:image", Content: imageURL}, } @@ -261,20 +256,21 @@ func (l *Linker) GenerateVideoCard(ctx context.Context, u *url.URL, vv *streampl } } - // do twitter/og title after - metaTags = append(metaTags, MetaTag{ - Type: "property", - Key: "og:title", - Content: fmt.Sprintf("%s%s", titleStr, brandingTitle), - }) - metaTags = append(metaTags, MetaTag{ - Type: "name", - Key: "twitter:title", - Content: fmt.Sprintf("%s%s", titleStr, brandingTitle), - }) + // The card headline is the video's own title; the author + node name go + // in the description. Appended after the branding loop so the description + // can reference the resolved branding title. + title := video.Title + description := fmt.Sprintf("@%s's video on %s", authorHandle, brandingTitle) + metaTags = append(metaTags, + MetaTag{Type: "name", Key: "description", Content: description}, + MetaTag{Type: "property", Key: "og:description", Content: description}, + MetaTag{Type: "property", Key: "og:title", Content: title}, + MetaTag{Type: "name", Key: "twitter:description", Content: description}, + MetaTag{Type: "name", Key: "twitter:title", Content: title}, + ) return l.GenerateHTML(ctx, &PageConfig{ - Title: fmt.Sprintf("%s%s", titleStr, brandingTitle), + Title: title, Metas: metaTags, SentryDSN: sentryDSN, }) diff --git a/pkg/linking/linking_test.go b/pkg/linking/linking_test.go index e6b23de5e..2b9c4132b 100644 --- a/pkg/linking/linking_test.go +++ b/pkg/linking/linking_test.go @@ -105,7 +105,10 @@ func TestGenerateVideoCard(t *testing.T) { require.NoError(t, err) linkStr := string(linkCard) require.True(t, strings.Contains(linkStr, "iame.li"), "should contain the author handle") - require.True(t, strings.Contains(linkStr, video.Title), "should contain the video title") + require.True(t, strings.Contains(linkStr, "<title>"+video.Title+""), + "page title should be the video's own title") + require.True(t, strings.Contains(linkStr, `content="`+video.Title+`"`), + "og:title/twitter:title should be the video's own title") require.True(t, strings.Contains(linkStr, "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:2zmxikig2sj7gqaezl5gntae/"+thumbCID+"@jpeg"), "og:image should be the video thumbnail served via the bsky CDN") diff --git a/pkg/model/video.go b/pkg/model/video.go index 5ac3de687..ae4f8436f 100644 --- a/pkg/model/video.go +++ b/pkg/model/video.go @@ -114,12 +114,36 @@ func (m *DBModel) GetVideoView(ctx context.Context, uri string) (*streamplace.Me return nil, err } + tracks := []*streamplace.MediaTrack_TrackView{} + if rec.Source.MediaDefs_SourceTracks != nil { + for _, track := range rec.Source.MediaDefs_SourceTracks.Tracks { + t, err := m.GetMediaTrackByURI(ctx, track.Uri) + if err != nil { + return nil, fmt.Errorf("get media track by uri: %w", err) + } + if t == nil { + continue + } + cid, err := spid.GetCID(t) + if err != nil { + return nil, fmt.Errorf("get cid: %w", err) + } + tracks = append(tracks, &streamplace.MediaTrack_TrackView{ + Record: &lexutil.LexiconTypeDecoder{Val: t}, + Uri: track.Uri, + Cid: cid.String(), + Author: author, + }) + } + } + return &streamplace.MediaGetVideo_VideoView{ Uri: row.URI, Cid: row.CID, Author: author, Record: &lexutil.LexiconTypeDecoder{Val: rec}, ViewCounts: summary, + Tracks: tracks, }, nil } diff --git a/pkg/streamplace/mediagetVideo.go b/pkg/streamplace/mediagetVideo.go index 7f365c975..028b80e19 100644 --- a/pkg/streamplace/mediagetVideo.go +++ b/pkg/streamplace/mediagetVideo.go @@ -16,6 +16,7 @@ type MediaGetVideo_VideoView struct { Author *appbsky.ActorDefs_ProfileViewBasic `json:"author" cborgen:"author"` Cid string `json:"cid" cborgen:"cid"` Record *lexutil.LexiconTypeDecoder `json:"record" cborgen:"record"` + Tracks []*MediaTrack_TrackView `json:"tracks,omitempty" cborgen:"tracks,omitempty"` Uri string `json:"uri" cborgen:"uri"` // viewCounts: Aggregated view counts across every indexed reporter. Always present; zero-valued (count=0, reporters=0) when no place.stream.media.viewCount records have been observed yet, so consumers can render a count unconditionally. ViewCounts *MediaGetVideo_ViewCountSummary `json:"viewCounts" cborgen:"viewCounts"` diff --git a/pkg/streamplace/mediatrack.go b/pkg/streamplace/mediatrack.go index 35d00403c..38558888c 100644 --- a/pkg/streamplace/mediatrack.go +++ b/pkg/streamplace/mediatrack.go @@ -11,6 +11,7 @@ import ( "io" comatproto "github.com/bluesky-social/indigo/api/atproto" + appbsky "github.com/bluesky-social/indigo/api/bsky" lexutil "github.com/bluesky-social/indigo/lex/util" cbg "github.com/whyrusleeping/cbor-gen" ) @@ -149,3 +150,11 @@ func (t *MediaTrack_Track) UnmarshalCBOR(r io.Reader) error { return nil } } + +// MediaTrack_TrackView is a "trackView" in the place.stream.media.track schema. +type MediaTrack_TrackView struct { + Author *appbsky.ActorDefs_ProfileViewBasic `json:"author" cborgen:"author"` + Cid string `json:"cid" cborgen:"cid"` + Record *lexutil.LexiconTypeDecoder `json:"record" cborgen:"record"` + Uri string `json:"uri" cborgen:"uri"` +} -- 2.51.2 From 32a222ba54c2d512f762edcc6989c3aa284a0e8a Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Fri, 29 May 2026 22:16:58 -0500 Subject: [PATCH 10/47] likes and comments --- js/app/components/mobile/player.tsx | 43 +- .../components/mobile-player/video-retry.tsx | 24 +- .../src/components/vod/vod-comments.tsx | 173 ++++ .../src/components/vod/vod-like-button.tsx | 77 ++ .../components/vod/vod-mobile-metadata.tsx | 56 + .../src/components/vod/vod-section.tsx | 28 + js/components/src/index.tsx | 7 + .../src/vod-store/vod-interactions.tsx | 173 ++++ .../place-stream-moderation-createvodgate.md | 117 +++ .../place-stream-moderation-deletevodgate.md | 101 ++ .../place-stream-moderation-permission.md | 8 +- .../content/docs/lex-reference/openapi.json | 957 ++++++++++++------ .../vod/place-stream-vod-comment.md | 109 ++ .../vod/place-stream-vod-defs.md | 73 ++ .../vod/place-stream-vod-gate.md | 53 + .../vod/place-stream-vod-getcomments.md | 94 ++ .../vod/place-stream-vod-getlikes.md | 148 +++ .../vod/place-stream-vod-like.md | 59 ++ .../stream/moderation/createVodGate.json | 62 ++ .../stream/moderation/deleteVodGate.json | 50 + .../place/stream/moderation/permission.json | 8 +- lexicons/place/stream/vod/comment.json | 56 + lexicons/place/stream/vod/defs.json | 29 + lexicons/place/stream/vod/gate.json | 22 + lexicons/place/stream/vod/getComments.json | 49 + lexicons/place/stream/vod/getLikes.json | 69 ++ lexicons/place/stream/vod/like.json | 27 + mise.toml | 1 + pkg/atproto/firehose.go | 21 + pkg/atproto/sync.go | 127 +++ pkg/constants/constants.go | 3 + pkg/gen/gen.go | 4 + pkg/model/model.go | 22 + pkg/model/vod_comment.go | 141 +++ pkg/model/vod_gate.go | 55 + pkg/model/vod_like.go | 119 +++ pkg/moderation/permissions.go | 3 + pkg/spxrpc/place_stream_moderation.go | 96 ++ pkg/spxrpc/place_stream_vod.go | 80 ++ pkg/spxrpc/stubs.go | 92 ++ pkg/streamplace/cbor_gen.go | 756 ++++++++++++++ pkg/streamplace/moderationcreateVodGate.go | 37 + pkg/streamplace/moderationdeleteVodGate.go | 33 + pkg/streamplace/vodcomment.go | 33 + pkg/streamplace/voddefs.go | 53 + pkg/streamplace/vodgate.go | 19 + pkg/streamplace/vodgetComments.go | 40 + pkg/streamplace/vodgetLikes.go | 53 + pkg/streamplace/vodlike.go | 21 + 49 files changed, 4158 insertions(+), 323 deletions(-) create mode 100644 js/components/src/components/vod/vod-comments.tsx create mode 100644 js/components/src/components/vod/vod-like-button.tsx create mode 100644 js/components/src/components/vod/vod-mobile-metadata.tsx create mode 100644 js/components/src/components/vod/vod-section.tsx create mode 100644 js/components/src/vod-store/vod-interactions.tsx create mode 100644 js/docs/src/content/docs/lex-reference/moderation/place-stream-moderation-createvodgate.md create mode 100644 js/docs/src/content/docs/lex-reference/moderation/place-stream-moderation-deletevodgate.md create mode 100644 js/docs/src/content/docs/lex-reference/vod/place-stream-vod-comment.md create mode 100644 js/docs/src/content/docs/lex-reference/vod/place-stream-vod-defs.md create mode 100644 js/docs/src/content/docs/lex-reference/vod/place-stream-vod-gate.md create mode 100644 js/docs/src/content/docs/lex-reference/vod/place-stream-vod-getcomments.md create mode 100644 js/docs/src/content/docs/lex-reference/vod/place-stream-vod-getlikes.md create mode 100644 js/docs/src/content/docs/lex-reference/vod/place-stream-vod-like.md create mode 100644 lexicons/place/stream/moderation/createVodGate.json create mode 100644 lexicons/place/stream/moderation/deleteVodGate.json create mode 100644 lexicons/place/stream/vod/comment.json create mode 100644 lexicons/place/stream/vod/defs.json create mode 100644 lexicons/place/stream/vod/gate.json create mode 100644 lexicons/place/stream/vod/getComments.json create mode 100644 lexicons/place/stream/vod/getLikes.json create mode 100644 lexicons/place/stream/vod/like.json create mode 100644 pkg/model/vod_comment.go create mode 100644 pkg/model/vod_gate.go create mode 100644 pkg/model/vod_like.go create mode 100644 pkg/spxrpc/place_stream_vod.go create mode 100644 pkg/streamplace/moderationcreateVodGate.go create mode 100644 pkg/streamplace/moderationdeleteVodGate.go create mode 100644 pkg/streamplace/vodcomment.go create mode 100644 pkg/streamplace/voddefs.go create mode 100644 pkg/streamplace/vodgate.go create mode 100644 pkg/streamplace/vodgetComments.go create mode 100644 pkg/streamplace/vodgetLikes.go create mode 100644 pkg/streamplace/vodlike.go diff --git a/js/app/components/mobile/player.tsx b/js/app/components/mobile/player.tsx index 997f3b22d..037351ab7 100644 --- a/js/app/components/mobile/player.tsx +++ b/js/app/components/mobile/player.tsx @@ -18,6 +18,7 @@ import { useSegmentDimensions, VideoProvider, View, + VodSection, } from "@streamplace/components"; import { gap, h, pt, w } from "@streamplace/components/src/lib/theme/atoms"; import { useLiveUser } from "hooks/useLiveUser"; @@ -349,11 +350,20 @@ export function PlayerInner( }, [props.showUnavailable]); // content info - const { width, height } = usePlayerDimensions(); + const { width: pwidth, height: pheight } = usePlayerDimensions(); + + console.log(pwidth, pheight); + + let width = pwidth > 0 && props.mode !== "vod" ? pwidth : 16; + let height = pheight > 0 && props.mode !== "vod" ? pheight : 9; + + console.log(width, height); // Calculate aspect ratio and determine if we're in desktop mode const aspectRatio = width > 0 && height > 0 ? width / height : 16 / 9; + console.log(aspectRatio, 16 / 9); + // on mobile we want to hide the sidebar when going fullscreen useEffect(() => { if (Platform.OS !== "web" && width > height) { @@ -386,6 +396,12 @@ export function PlayerInner( const isPlayerRatioGreater = aspectRatio >= 16 / 9; + const { height: windowHeight } = useWindowDimensions(); + const vodMobileHeight = + props.mode === "vod" + ? Math.min(contentWidth / aspectRatio, windowHeight * 0.7) + : undefined; + // animated style for offline height transition const animatedHeightStyle = useAnimatedStyle(() => { return { @@ -408,11 +424,13 @@ export function PlayerInner( flexGrow: 1, // This makes content expand to fill available space minHeight: "100%", // Ensures minimum height } - : { - flex: 1, - } + : props.mode === "vod" + ? { flexGrow: 1 } + : { + flex: 1, + } } - scrollEnabled={showFullDesktopMode} + scrollEnabled={showFullDesktopMode || props.mode === "vod"} bounces={false} showsVerticalScrollIndicator={false} > @@ -422,10 +440,15 @@ export function PlayerInner( ? { width: calculatedWidth, } - : { - flex: 1, - maxHeight: "auto", - }, + : props.mode === "vod" && vodMobileHeight + ? { + width: "100%" as any, + height: vodMobileHeight, + } + : { + flex: 1, + maxHeight: "auto", + }, { paddingTop: isPlayerRatioGreater && !isLandscape && !props.showUnavailable @@ -444,6 +467,7 @@ export function PlayerInner( ) : ( (isLandscape || props.mode === "vod") && ( @@ -473,6 +497,7 @@ export function PlayerInner( showChat={props.showChat} /> )} + {props.mode === "vod" && } ); } diff --git a/js/components/src/components/mobile-player/video-retry.tsx b/js/components/src/components/mobile-player/video-retry.tsx index a7e461f0c..296aa8281 100644 --- a/js/components/src/components/mobile-player/video-retry.tsx +++ b/js/components/src/components/mobile-player/video-retry.tsx @@ -5,16 +5,15 @@ export default function VideoRetry(props: { children: React.ReactNode }) { const retryTimeoutRef = useRef(null); const [retries, setRetries] = useState(0); const playing = usePlayerStore((x) => x.status === PlayerStatus.PLAYING); + const mode = usePlayerStore((x) => x.mode); + const status = usePlayerStore((x) => x.status); const [lastChange, setLastChange] = useState(Date.now()); const [tick, setTick] = useState(0); - const status = usePlayerStore((x) => x.status); useEffect(() => { setLastChange(Date.now()); }, [status]); - const mode = usePlayerStore((x) => x.mode); - useEffect(() => { if (mode === "vod") return; if (!playing) { @@ -28,7 +27,15 @@ export default function VideoRetry(props: { children: React.ReactNode }) { const stalledFor5sec = !playing && tick - lastChange > 5000; useEffect(() => { - if (!playing) { + if (stalledFor5sec) { + console.log("Stalled for 5 seconds, retrying..."); + setRetries((r) => r + 1); + } + }, [stalledFor5sec]); + + useEffect(() => { + if (mode === "vod") return; + if (!playing && status !== PlayerStatus.PAUSE) { const doRetry = () => { console.log("Retrying video playback..."); setRetries((prevRetries) => prevRetries + 1); @@ -44,14 +51,7 @@ export default function VideoRetry(props: { children: React.ReactNode }) { retryTimeoutRef.current = null; } }; - }, [!playing, mode]); - - useEffect(() => { - if (stalledFor5sec) { - console.log("Stalled for 5 seconds, retrying..."); - setRetries((r) => r + 1); - } - }, [stalledFor5sec]); + }, [!playing, mode, status]); return {props.children}; } diff --git a/js/components/src/components/vod/vod-comments.tsx b/js/components/src/components/vod/vod-comments.tsx new file mode 100644 index 000000000..1d7b99de9 --- /dev/null +++ b/js/components/src/components/vod/vod-comments.tsx @@ -0,0 +1,173 @@ +import { useCallback, useEffect, useState } from "react"; +import { + ActivityIndicator, + FlatList, + TouchableOpacity, + View, +} from "react-native"; +import type { PlaceStreamVodDefs } from "streamplace"; +import { + borders, + flex, + gap, + layout, + mb, + mt, + pt, + px, + py, + r, + useTheme, +} from "../../ui"; +import { + useCreateVodComment, + useGetVodComments, +} from "../../vod-store/vod-interactions"; +import { Text } from "../ui/text"; +import { Textarea } from "../ui/textarea"; + +export function VodComments({ videoUri }: { videoUri: string }) { + const [comments, setComments] = useState( + [], + ); + const [loading, setLoading] = useState(true); + const [draft, setDraft] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [cursor, setCursor] = useState(); + + const getComments = useGetVodComments(); + const createComment = useCreateVodComment(); + const { theme } = useTheme(); + + const loadComments = useCallback(async () => { + setLoading(true); + try { + const result = await getComments(videoUri, 50, cursor); + setComments((prev) => + cursor ? [...prev, ...result.comments] : result.comments, + ); + setCursor(result.cursor); + } catch (e) { + console.error("Failed to load comments", e); + } finally { + setLoading(false); + } + }, [videoUri, cursor, getComments]); + + useEffect(() => { + setCursor(undefined); + loadComments(); + }, [videoUri]); + + const handleSubmit = useCallback(async () => { + if (!draft.trim()) return; + setSubmitting(true); + try { + await createComment({ text: draft.trim(), video: videoUri }); + setDraft(""); + setCursor(undefined); + await loadComments(); + } catch (e) { + console.error("Failed to submit comment", e); + } finally { + setSubmitting(false); + } + }, [draft, videoUri, createComment, loadComments]); + + const renderComment = useCallback( + ({ item }: { item: PlaceStreamVodDefs.CommentView }) => { + const record = item.record as any; + return ( + + + + {item.author.handle || item.author.did} + + + {new Date(item.indexedAt).toLocaleDateString()} + + + {record.text} + + + {item.likeCount > 0 ? `${item.likeCount} likes` : ""} + + + + ); + }, + [theme], + ); + + return ( + + + Comments + + + {loading && comments.length === 0 ? ( + + ) : ( + item.uri} + onEndReached={cursor ? loadComments : undefined} + onEndReachedThreshold={0.5} + style={flex.values[1]} + /> + )} + + +