diff --git a/js/app/components/mobile/player.tsx b/js/app/components/mobile/player.tsx index 997f3b22..037351ab 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 a7e461f0..296aa828 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 00000000..1d7b99de --- /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]} + /> + )} + + +