diff --git a/js/app/src/screens/vod-embed.tsx b/js/app/src/screens/vod-embed.tsx index e01fe15e..c17f4a5e 100644 --- a/js/app/src/screens/vod-embed.tsx +++ b/js/app/src/screens/vod-embed.tsx @@ -1,12 +1,18 @@ -import { VideoProvider, View, VodPlayer, zero } from "@streamplace/components"; +import { + LivestreamProvider, + VideoProvider, + View, + VodPlayer, + zero, +} from "@streamplace/components"; import { Redirect } from "components/aqlink"; +import { BottomControlBar } from "components/mobile/desktop-ui/bottom-controls"; +import { TopControlBar } from "components/mobile/desktop-ui/top-controls"; import { useEffect } from "react"; import { useStore } from "store"; -// Chrome-less VOD player for iframe embeds (/embed/:user/video/:tid). Mirrors -// the live EmbedScreen: hide the sidebar and render just the minimal -// (expo-video's native controls handle play/scrub) with no back -// button or surrounding metadata. +const { layout, h, w, position, px, py } = zero; + export default function VodEmbedScreen({ route, }: { @@ -29,9 +35,61 @@ export default function VodEmbedScreen({ return ( - - - + + + + + + {}} + embedded={true} + /> + + + + {}} + showChat={false} + /> + + + + + + ); } diff --git a/js/components/src/components/mobile-player/ui/seek-bar.tsx b/js/components/src/components/mobile-player/ui/seek-bar.tsx index eff71d86..2154e2e1 100644 --- a/js/components/src/components/mobile-player/ui/seek-bar.tsx +++ b/js/components/src/components/mobile-player/ui/seek-bar.tsx @@ -5,8 +5,7 @@ import { usePlayerStore } from "../../../player-store"; const TRACK_HEIGHT = 3; const THUMB_SIZE = 14; -// A tall, invisible touch strip so the thin bar is easy to grab/drag. -const TOUCH_HEIGHT = 28; +const TOUCH_HEIGHT = 16; // Native VOD scrub bar. The old implementation leaned on @rn-primitives/slider // driven by web pointer events (onPointerUp/onPointerEnter), which never fire @@ -64,7 +63,7 @@ export function SeekBar() { const bufferedPct = Math.max(0, Math.min(1, bufferedEnd / duration)) * 100; return ( - + diff --git a/js/components/src/components/vod/vod-player.tsx b/js/components/src/components/vod/vod-player.tsx index 8d37770b..4361f31b 100644 --- a/js/components/src/components/vod/vod-player.tsx +++ b/js/components/src/components/vod/vod-player.tsx @@ -4,24 +4,20 @@ import { PlayerProvider } from "../../player-store/player-provider"; import { usePlayerStore } from "../../player-store/player-store"; import Video from "../mobile-player/video"; -// A deliberately minimal VOD player: a PlayerProvider, the src pushed into the -// player store in "vod" mode, and the platform diff --git a/js/app/src/screens/vod-embed.tsx b/js/app/src/screens/vod-embed.tsx index c17f4a5e..eb6dec83 100644 --- a/js/app/src/screens/vod-embed.tsx +++ b/js/app/src/screens/vod-embed.tsx @@ -6,13 +6,10 @@ import { zero, } from "@streamplace/components"; import { Redirect } from "components/aqlink"; -import { BottomControlBar } from "components/mobile/desktop-ui/bottom-controls"; -import { TopControlBar } from "components/mobile/desktop-ui/top-controls"; +import { DesktopUi } from "components/mobile/desktop-ui"; import { useEffect } from "react"; import { useStore } from "store"; -const { layout, h, w, position, px, py } = zero; - export default function VodEmbedScreen({ route, }: { @@ -37,56 +34,8 @@ export default function VodEmbedScreen({ - - - - {}} - embedded={true} - /> - - - - {}} - showChat={false} - /> - - - + + diff --git a/js/components/src/components/mobile-player/video.tsx b/js/components/src/components/mobile-player/video.tsx index 768d3b4c..95ce7336 100644 --- a/js/components/src/components/mobile-player/video.tsx +++ b/js/components/src/components/mobile-player/video.tsx @@ -139,6 +139,7 @@ const VideoElement = forwardRef< VideoProps & { videoRef?: React.RefObject } >((props, ref) => { const x = usePlayerStore((x) => x); + const mode = usePlayerStore((x) => x.mode); const url = useStreamplaceStore((x) => x.url); const playerEvent = usePlayerStore((x) => x.playerEvent); const setMuteWasForced = usePlayerStore((x) => x.setMuteWasForced); @@ -204,7 +205,9 @@ const VideoElement = forwardRef< localVideoRef.current.play().catch((err) => { console.log("error playing video", err.name); if (err.name === "NotAllowedError") { - if (localVideoRef.current) { + if (mode === "vod") { + setAutoplayFailed(true); + } else if (localVideoRef.current) { console.log("Setting muted and retrying"); setMuted(true); localVideoRef.current.muted = true; diff --git a/js/components/src/components/vod/vod-player.tsx b/js/components/src/components/vod/vod-player.tsx index 4361f31b..01d55963 100644 --- a/js/components/src/components/vod/vod-player.tsx +++ b/js/components/src/components/vod/vod-player.tsx @@ -2,20 +2,33 @@ import { useEffect } from "react"; import { View } from "react-native"; import { PlayerProvider } from "../../player-store/player-provider"; import { usePlayerStore } from "../../player-store/player-store"; +import { useMuted, useSetMuted } from "../../streamplace-store"; import Video from "../mobile-player/video"; export function VodPlayer({ src, objectFit = "contain", + embedded, + muted: mutedProp, + pictureInPictureEnabled, children, }: { src: string; objectFit?: "contain" | "cover"; + embedded?: boolean; + muted?: boolean; + pictureInPictureEnabled?: boolean; children?: React.ReactNode; }) { return ( - + {children} @@ -25,24 +38,53 @@ export function VodPlayer({ function VodPlayerInner({ src, objectFit, + embedded, + muted: mutedProp, + pictureInPictureEnabled, children, }: { src: string; objectFit: "contain" | "cover"; + embedded: boolean; + muted?: boolean; + pictureInPictureEnabled?: boolean; children?: React.ReactNode; }) { const setSrc = usePlayerStore((x) => x.setSrc); const setMode = usePlayerStore((x) => x.setMode); + const setEmbedded = usePlayerStore((x) => x.setEmbedded); const storeSrc = usePlayerStore((x) => x.src); + const setMuted = useSetMuted(); + const muted = useMuted(); + useEffect(() => { setMode("vod"); setSrc(src); }, [src, setMode, setSrc]); + useEffect(() => { + setEmbedded(embedded); + }, [embedded, setEmbedded]); + + useEffect(() => { + if (mutedProp !== undefined) { + let wasMuted: boolean | null = muted; + setTimeout(() => setMuted(mutedProp), 200); + return () => { + if (wasMuted !== null) setMuted(wasMuted); + }; + } + }, [mutedProp]); + return ( - {storeSrc === src ? ); -- 2.51.2 From b966eb0c53fdadb1e089ba6ba2feb896ca90e363 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Thu, 4 Jun 2026 19:45:39 -0500 Subject: [PATCH 3/5] Add visual play/pause indicator to desktop UI - Introduce `PlayPauseIndicator` component for state transition feedback - Remove manual play button from loading overlay - Refine control bar visibility logic to persist when paused --- js/app/components/mobile/desktop-ui.tsx | 60 +++++++------ js/app/components/mobile/desktop-ui/index.ts | 1 + .../mobile/desktop-ui/mute-overlay.tsx | 1 + .../desktop-ui/play-pause-indicator.tsx | 90 +++++++++++++++++++ .../mobile-player/ui/autoplay-button.tsx | 1 + .../ui/viewer-loading-overlay.tsx | 79 +++++----------- 6 files changed, 147 insertions(+), 85 deletions(-) create mode 100644 js/app/components/mobile/desktop-ui/play-pause-indicator.tsx diff --git a/js/app/components/mobile/desktop-ui.tsx b/js/app/components/mobile/desktop-ui.tsx index 7050fbba..37dbf2ef 100644 --- a/js/app/components/mobile/desktop-ui.tsx +++ b/js/app/components/mobile/desktop-ui.tsx @@ -1,4 +1,5 @@ import { + PlayerStatus, PlayerUI, PortalHost, Toast, @@ -26,6 +27,7 @@ import { MuteOverlay, TopControlBar, } from "./desktop-ui/index"; +import { PlayPauseIndicator } from "./desktop-ui/play-pause-indicator"; import { useResponsiveLayout } from "./useResponsiveLayout"; const { h, layout, position, w, px, py, r, p } = zero; @@ -72,6 +74,7 @@ export function DesktopUi({ const fullscreen = usePlayerStore((state) => state.fullscreen); const setFullscreen = usePlayerStore((state) => state.setFullscreen); const selectedRendition = usePlayerStore((state) => state.selectedRendition); + const status = usePlayerStore((state) => state.status); const safeAreaInsets = embedded ? { ...originalSafeAreaInsets, top: 0 } @@ -96,12 +99,13 @@ export function DesktopUi({ if (selectedRendition === "audio") return; if (ingest !== null) return; + if (status === PlayerStatus.PAUSE) return; fadeTimeout.current = setTimeout(() => { fadeOpacity.value = withTiming(0, { duration: 400 }); setIsControlsVisible(false); }, FADE_OUT_DELAY); - }, [fadeOpacity, selectedRendition, ingest]); + }, [fadeOpacity, selectedRendition, ingest, status]); const onPlayerHover = useCallback(() => { resetFadeTimer(); @@ -225,8 +229,8 @@ export function DesktopUi({ collapsable={false} > - + )} - - - - - - {isSelfAndNotLive && ( + + + + + {fullscreen && } ); diff --git a/js/app/components/mobile/desktop-ui/index.ts b/js/app/components/mobile/desktop-ui/index.ts index 91c372da..3b8f0ba8 100644 --- a/js/app/components/mobile/desktop-ui/index.ts +++ b/js/app/components/mobile/desktop-ui/index.ts @@ -2,5 +2,6 @@ export { BottomControlBar } from "./bottom-controls"; export { KebabMenu } from "./kebab"; export { LiveBubble } from "./live-bubble"; export { MuteOverlay } from "./mute-overlay"; +export { PlayPauseIndicator } from "./play-pause-indicator"; export { TopControlBar } from "./top-controls"; export { VolumeSlider } from "./volume-slider"; diff --git a/js/app/components/mobile/desktop-ui/mute-overlay.tsx b/js/app/components/mobile/desktop-ui/mute-overlay.tsx index 09ed23ff..565647da 100644 --- a/js/app/components/mobile/desktop-ui/mute-overlay.tsx +++ b/js/app/components/mobile/desktop-ui/mute-overlay.tsx @@ -35,6 +35,7 @@ export function MuteOverlay() { h.percent[100], w.percent[100], ]} + pointerEvents="box-none" > { diff --git a/js/app/components/mobile/desktop-ui/play-pause-indicator.tsx b/js/app/components/mobile/desktop-ui/play-pause-indicator.tsx new file mode 100644 index 00000000..1827414d --- /dev/null +++ b/js/app/components/mobile/desktop-ui/play-pause-indicator.tsx @@ -0,0 +1,90 @@ +import { PlayerStatus, usePlayerStore } from "@streamplace/components"; +import { Pause, Play } from "lucide-react-native"; +import { useCallback, useEffect, useRef } from "react"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withSequence, + withTiming, +} from "react-native-reanimated"; + +export function PlayPauseIndicator() { + const status = usePlayerStore((x) => x.status); + const prevStatus = useRef(status); + const opacity = useSharedValue(0); + const scale = useSharedValue(0.5); + + const animate = useCallback( + (toPlaying: boolean) => { + opacity.value = 1; + opacity.value = withSequence( + withTiming(1, { duration: 100 }), + withTiming(1, { duration: 400 }), + withTiming(0, { duration: 300 }), + ); + if (toPlaying) { + scale.value = 0.8; + scale.value = withSequence( + withTiming(1, { duration: 100 }), + withTiming(1, { duration: 400 }), + withTiming(1.3, { duration: 300 }), + ); + } else { + scale.value = 1; + scale.value = withSequence( + withTiming(0.8, { duration: 100 }), + withTiming(0.8, { duration: 500 }), + ); + } + }, + [opacity, scale], + ); + + useEffect(() => { + if (status !== prevStatus.current) { + const wasPlaying = prevStatus.current === PlayerStatus.PLAYING; + const isPlaying = status === PlayerStatus.PLAYING; + const isPaused = status === PlayerStatus.PAUSE; + + if (isPlaying || isPaused) { + animate(isPlaying); + } + prevStatus.current = status; + } + }, [status, animate]); + + const animatedStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + transform: [{ scale: scale.value }], + })); + + const isPlaying = status === PlayerStatus.PLAYING; + + return ( + + {isPlaying ? ( + + ) : ( + + )} + + ); +} diff --git a/js/components/src/components/mobile-player/ui/autoplay-button.tsx b/js/components/src/components/mobile-player/ui/autoplay-button.tsx index 069558d3..8636e930 100644 --- a/js/components/src/components/mobile-player/ui/autoplay-button.tsx +++ b/js/components/src/components/mobile-player/ui/autoplay-button.tsx @@ -50,6 +50,7 @@ export function AutoplayButton() { h.percent[100], w.percent[100], ]} + pointerEvents="box-none" > x.status); - const togglePlayPause = usePlayerStore((x) => x.togglePlayPause); - const { theme, zero: zt } = useTheme(); const opacity = useSharedValue(0); useEffect(() => { @@ -28,60 +18,33 @@ export function ViewerLoadingOverlay() { } }, [status, opacity]); - const animatedStyle = useAnimatedStyle(() => { - return { - opacity: opacity.value, - }; - }); + const animatedStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + })); if (status === PlayerStatus.PLAYING) { return ; } - if (status === PlayerStatus.SUSPEND) { - return null; // No overlay when stopped + if (status === PlayerStatus.SUSPEND || status === PlayerStatus.PAUSE) { + return null; } - const isPaused = status === PlayerStatus.PAUSE; - return ( - <> - - {!isPaused && } - - {isPaused && ( - - - - )} - + + + ); } -- 2.51.2 From cc7f9d2bcfd79cf2421183cd67442786fd2769f0 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Thu, 4 Jun 2026 20:31:07 -0500 Subject: [PATCH 4/5] show chat on live, show chrome on mobile web portrait --- js/app/components/mobile/player.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/js/app/components/mobile/player.tsx b/js/app/components/mobile/player.tsx index 3d0777ca..6329eee0 100644 --- a/js/app/components/mobile/player.tsx +++ b/js/app/components/mobile/player.tsx @@ -82,18 +82,18 @@ function PlayerWithProvider( onTeleport?: (targetHandle: string, targetDID: string) => void; }, ) { - let [showChat, setShowChat] = useState(props.mode === "live"); + let [showChat, setShowChat] = useState(props.mode != "vod"); if (props.mode === "vod") { showChat = false; } const { shouldShowChatSidePanel, chatPanelWidth } = useResponsiveLayout(); const chatVisible = shouldShowChatSidePanel && showChat; const { width: screenWidth, height: screenHeight } = useWindowDimensions(); - const { top: safeTop } = useSafeAreaInsets(); + let { top: safeTop } = useSafeAreaInsets(); const segDims = useSegmentDimensions(); const isPortrait = screenHeight > screenWidth; + // if the screen is portrait and video is landscaps const isPortraitLandscapeCase = - Platform.OS !== "web" && isPortrait && segDims.width > segDims.height && !shouldShowChatSidePanel && @@ -176,9 +176,7 @@ function PlayerWithProvider( chatSection = ( <> - {!showUnavailable && ( - - )} + ); } else if (shouldShowChatSidePanel) { @@ -233,7 +231,6 @@ function PlayerWithProvider( Back - {chatSection} ); @@ -263,7 +260,10 @@ function PlayerWithProvider( flex: 1, width: "100%", height: "100%", - paddingTop: isPortraitLandscapeCase ? 54 : undefined, + paddingTop: + isPortraitLandscapeCase && Platform.OS != "web" + ? 54 + : undefined, }, ]} > -- 2.51.2 From c35a8679dbcb50ed2d746a4b4ff31d8a9b2fca80 Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Thu, 4 Jun 2026 22:15:54 -0500 Subject: [PATCH 5/5] Improve mobile VOD player layout and metadata - Add time display to VOD controls. - Update landscape video player sizing and positioning. - Redesign mobile metadata to include tag support and a scrollable action bar. - Add animation to the Like button. --- js/app/components/mobile/player.tsx | 7 +- js/app/components/mobile/ui.tsx | 4 +- .../components/mobile-player/ui/viewers.tsx | 1 - .../mobile-player/ui/vod-controls.tsx | 22 +++- .../src/components/vod/like-button.tsx | 37 ++++-- .../src/components/vod/vod-description.tsx | 43 +++++-- .../components/vod/vod-mobile-metadata.tsx | 105 +++++++++++------- 7 files changed, 156 insertions(+), 63 deletions(-) diff --git a/js/app/components/mobile/player.tsx b/js/app/components/mobile/player.tsx index 6329eee0..f176f999 100644 --- a/js/app/components/mobile/player.tsx +++ b/js/app/components/mobile/player.tsx @@ -455,12 +455,17 @@ export function PlayerInner( aspectRatio: vodAspectRatio, // Cap so a landscape video fits (letterboxed via objectFit:contain) // instead of clipping; flexShrink:0 keeps it from collapsing. - maxHeight: winHeight * 0.7, + maxHeight: isLandscape ? winHeight : winHeight * 0.7, + // center the video horizontally when in landscape since it won't fill the full width + marginHorizontal: isLandscape + ? (winWidth - Math.min(winWidth, winHeight * vodAspectRatio)) / 2 + : 0, flexShrink: 0, }} > {videoContent} + {/* will get pushed below the video if landscape so probably fine? */} ); diff --git a/js/app/components/mobile/ui.tsx b/js/app/components/mobile/ui.tsx index 5c6d7107..9243809e 100644 --- a/js/app/components/mobile/ui.tsx +++ b/js/app/components/mobile/ui.tsx @@ -306,15 +306,15 @@ export function MobileUi({ {mode === "vod" && ( - + )} {chatSection} diff --git a/js/components/src/components/mobile-player/ui/viewers.tsx b/js/components/src/components/mobile-player/ui/viewers.tsx index 28f129f9..9789445b 100644 --- a/js/components/src/components/mobile-player/ui/viewers.tsx +++ b/js/components/src/components/mobile-player/ui/viewers.tsx @@ -29,7 +29,6 @@ export function DehydratedViewers({ atoms.layout.flex.center, atoms.layout.flex.row, atoms.gap.all[2], - atoms.px[1], ]} > 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 97388ee5..2531a040 100644 --- a/js/components/src/components/mobile-player/ui/vod-controls.tsx +++ b/js/components/src/components/mobile-player/ui/vod-controls.tsx @@ -1,8 +1,8 @@ import { Gauge, Pause, Play } from "lucide-react-native"; import { Pressable } from "react-native"; +import { zero } from "../../.."; import { useLivestreamStore } from "../../../livestream-store"; import { PlayerStatus, usePlayerStore } from "../../../player-store"; - import { DropdownMenu, DropdownMenuGroup, @@ -15,6 +15,16 @@ import { View, } from "../../ui"; +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 VodControls() { const mode = usePlayerStore((x) => x.mode); const status = usePlayerStore((x) => x.status); @@ -26,6 +36,9 @@ export function VodControls() { const renditions = mode === "vod" ? vodLevels : liveRenditions; const th = useTheme(); + const playTime = usePlayerStore((x) => x.playTime); + const duration = usePlayerStore((x) => x.duration); + if (mode !== "vod") return null; const isPlaying = status === PlayerStatus.PLAYING; @@ -36,7 +49,7 @@ export function VodControls() { flexDirection: "row", alignItems: "center", paddingHorizontal: 16, - paddingVertical: 4, + paddingVertical: 10, gap: 12, }} > @@ -55,6 +68,11 @@ export function VodControls() { /> )} + + {formatTime(playTime)} + / + {formatTime(duration)} + diff --git a/js/components/src/components/vod/like-button.tsx b/js/components/src/components/vod/like-button.tsx index ec6d86f3..b669e5f6 100644 --- a/js/components/src/components/vod/like-button.tsx +++ b/js/components/src/components/vod/like-button.tsx @@ -1,7 +1,13 @@ +import { ThumbsUp } from "lucide-react-native"; import { useCallback, useEffect, useState } from "react"; import { TouchableOpacity, View } from "react-native"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withSpring, +} from "react-native-reanimated"; import { useDID } from "../../streamplace-store/streamplace-store"; -import { gap, layout, p, useTheme } from "../../ui"; +import { gap, layout, useTheme } from "../../ui"; import { useCreateLike, useDeleteLike, @@ -21,6 +27,12 @@ export function LikeButton({ subjectUri }: { subjectUri: string }) { const deleteLike = useDeleteLike(); const { theme } = useTheme(); + const scale = useSharedValue(1); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })); + const loadLikes = useCallback(async () => { try { const result = await getLikes(subjectUri, 50); @@ -40,6 +52,9 @@ export function LikeButton({ subjectUri }: { subjectUri: string }) { }, [loadLikes]); const toggleLike = useCallback(async () => { + scale.value = withSpring(1.05, { stiffness: 500, damping: 10 }, () => { + scale.value = withSpring(1, { stiffness: 500 }); + }); setLoading(true); try { if (userLiked && userLikeUri) { @@ -58,19 +73,19 @@ export function LikeButton({ subjectUri }: { subjectUri: string }) { } finally { setLoading(false); } - }, [userLiked, userLikeUri, subjectUri, createLike, deleteLike]); + }, [userLiked, userLikeUri, subjectUri, createLike, deleteLike, scale]); + + const heartColor = userLiked + ? theme.colors.background + : theme.colors.foreground; return ( - - - {userLiked ? "\u2764\uFE0F" : "\u2661"} - - - {likeCount} - + + + + + {likeCount} ); diff --git a/js/components/src/components/vod/vod-description.tsx b/js/components/src/components/vod/vod-description.tsx index 05f62255..1c013706 100644 --- a/js/components/src/components/vod/vod-description.tsx +++ b/js/components/src/components/vod/vod-description.tsx @@ -1,12 +1,9 @@ -import { View } from "react-native"; +import { ScrollView, View } from "react-native"; import type { PlaceStreamVideo } from "streamplace"; -import { mt, useTheme } from "../../ui"; +import { hexToRgba, mt, useTheme } from "../../ui"; import { useVideoStore } from "../../video-store/video-store"; import { Text } from "../ui/text"; -// The video's description, shown below the metadata. For now this is just the -// plain text below the video; a collapsible dropdown (and richtext facets) can -// come later. export function VodDescription() { const video = useVideoStore((x) => x.video); const { theme } = useTheme(); @@ -14,11 +11,43 @@ export function VodDescription() { if (!video) return null; const record = video.record as unknown as PlaceStreamVideo.Record; const description = record.description?.trim(); - if (!description) return null; + const tags = (record.tags as string[] | undefined) ?? []; return ( - {description} + {tags.length > 0 ? ( + + {tags.map((tag) => ( + + + {tag} + + + ))} + + ) : null} + {description ? ( + {description} + ) : null} ); } diff --git a/js/components/src/components/vod/vod-mobile-metadata.tsx b/js/components/src/components/vod/vod-mobile-metadata.tsx index 51c1a63f..e99e6c39 100644 --- a/js/components/src/components/vod/vod-mobile-metadata.tsx +++ b/js/components/src/components/vod/vod-mobile-metadata.tsx @@ -1,6 +1,5 @@ import { Image } from "expo-image"; -import { useWindowDimensions, View } from "react-native"; -import { zero } from "../.."; +import { ScrollView, useWindowDimensions, View } from "react-native"; import { useAuthor } from "../../hooks/useAuthor"; import { useAvatar } from "../../hooks/useAvatar"; import { useTitle } from "../../hooks/useTitle"; @@ -9,12 +8,10 @@ import { gap, layout, useTheme } from "../../ui"; import { useVideoStore } from "../../video-store/video-store"; import { Viewers } from "../mobile-player/ui/viewers"; import { ShareSheet } from "../share/sharesheet"; +import { Button } from "../ui"; import { Text } from "../ui/text"; import { LikeButton } from "./like-button"; -// Below this width we drop the avatar and the view count so the title, like -// and share controls don't crowd on small phones. Above it (tablets/desktop) -// the full row shows, matching what the desktop metadata bar used to carry. const NARROW_BREAKPOINT = 480; // rkeyFromAturi pulls the record key off an at:// URI @@ -49,25 +46,38 @@ export function VodMobileMetadata() { message: `Check out "${title || "this video"}" on Streamplace!`, }; - return ( + const handleText = ( + + {handle ? `@${handle}` : did} + + ); + + const actions = ( - {/* Left: avatar + title/author */} - - {wide && ( + + + {wide ? ( + + ) : null} + + + ); + + if (wide) { + return ( + + ) : null} - )} - - - {title || "Untitled"} - - - {handle ? `@${handle}` : did} - + + + + {title || "Untitled"} + + + + {handleText} + {actions} + + + ); + } - {/* Right: like + views + share */} - - {/* Use the server-canonical (DID-based) video.uri, not the store's - aturi — when the page is reached via a handle URL the aturi's - authority is the handle, and a like keyed on a handle subject - won't match the DID-keyed video record. */} - - {wide && } - - + return ( + + + {title || "Untitled"} + + {handleText} + + {actions} + ); }