From 32a222ba54c2d512f762edcc6989c3aa284a0e8a Mon Sep 17 00:00:00 2001 From: Natalie Bridgers Date: Fri, 29 May 2026 22:16:58 -0500 Subject: [PATCH] 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 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]} + /> + )} + + +