diff --git a/packages/react/src/components/player/PlayerStats.tsx b/packages/react/src/components/player/PlayerStats.tsx index 8c23cd266..1e2c6141e 100644 --- a/packages/react/src/components/player/PlayerStats.tsx +++ b/packages/react/src/components/player/PlayerStats.tsx @@ -7,7 +7,7 @@ import { localeAtom } from "@/store/i18n"; import { useAtomValue } from "jotai"; import { useTranslation } from "react-i18next"; -export function PlayerStats({ +export function VideoStats({ type, status, topic_id, diff --git a/packages/react/src/components/video/VideoCard.tsx b/packages/react/src/components/video/VideoCard.tsx index e4a9ab0eb..b5b8aa09d 100644 --- a/packages/react/src/components/video/VideoCard.tsx +++ b/packages/react/src/components/video/VideoCard.tsx @@ -217,7 +217,7 @@ export function VideoCard({ {(size == "lg" || size == "md") && video.channel && ( onClick ? onClick("channel", video, e) : goToVideoClickHandler(e) @@ -258,7 +258,7 @@ export function VideoCard({ {video.channel && ( onClick && onClick("channel", video, e)} > diff --git a/packages/react/src/components/video/VideoCard.utils.ts b/packages/react/src/components/video/VideoCard.utils.ts index f5bb1c47b..6fb93c652 100644 --- a/packages/react/src/components/video/VideoCard.utils.ts +++ b/packages/react/src/components/video/VideoCard.utils.ts @@ -29,7 +29,7 @@ export function useDefaultVideoCardClickHandler( // thumbnail, title, and channel image / channel name are all React Router links // in contrast, the rest of the video card is not a link const isChannelClick = - isLinkClick?.getAttribute("dataBehavior") === "channelLink"; + isLinkClick?.getAttribute("databehavior") === "channelLink"; if (isChannelClick) { return; // do not select, skip the entire handler diff --git a/packages/react/src/components/watch/Comments.tsx b/packages/react/src/components/watch/Comments.tsx new file mode 100644 index 000000000..125debb9d --- /dev/null +++ b/packages/react/src/components/watch/Comments.tsx @@ -0,0 +1,124 @@ +import React, { useState, useMemo, useRef, useEffect } from "react"; +import { ExternalLink } from "lucide-react"; +import { videoPlayerRefAtomFamily } from "@/store/player"; +import { useAtomValue } from "jotai"; + +interface CommentData { + message: string; + comment_key: string; +} + +interface TruncatedTextProps { + text: string; +} + +const TruncatedText = ({ text }: TruncatedTextProps) => { + const contentRef = useRef(null); + const [isClamped, setClamped] = useState(false); + + useEffect(() => { + if (contentRef && contentRef.current) { + setClamped( + contentRef.current.scrollHeight > contentRef.current.clientHeight + 2, // so i'm not too sure why but there's a 2px offset on my computer between the client height and the scrollheight. + ); + } + }, []); + + const [expanded, setExpanded] = useState(false); + return ( +
+
+ +
+ {isClamped && ( + + )} +
+ ); +}; + +const parseTimestamps = (message: string, videoId: string) => { + const decoder = document.createElement("div"); + decoder.innerHTML = message; + const sanitizedText = decoder.textContent || ""; + + return sanitizedText.replace( + /(?:([0-5]?[0-9]):)?([0-5]?[0-9]):([0-5][0-9])/gm, + (match, hr, min, sec) => { + const seconds = Number(hr ?? 0) * 3600 + Number(min) * 60 + Number(sec); + return `${match}`; + }, + ); +}; + +const Comment = ({ + comment, + videoId, +}: { + comment: CommentData; + videoId: string; +}) => { + const parsedMessage = useMemo( + () => parseTimestamps(comment.message, videoId), + [comment.message, videoId], + ); + + return ( +
+ + + + +
+ ); +}; + +export const Comments = ({ video }: { video?: PlaceholderVideo }) => { + const playerRefAtom = videoPlayerRefAtomFamily( + video?.id || "__nonexistent__", + ); + const player = useAtomValue(playerRefAtom); + const goToTimestampHandler = (e: React.MouseEvent) => { + if (e.target instanceof HTMLAnchorElement) { + if (e.target.dataset.time) { + console.log(e.target.dataset.time, player); + player?.seekTo(+e.target.dataset.time, "seconds"); + player?.getInternalPlayer().playVideo?.(); + e.preventDefault(); + } + } + }; + + if (!video?.comments?.length) return null; + return ( +
+ } + > + {video.comments.map((comment) => ( + + ))} +
+ ); +}; + +export default Comments; diff --git a/packages/react/src/react-router-dom.d.ts b/packages/react/src/react-router-dom.d.ts index 90b9c677d..0a33db422 100644 --- a/packages/react/src/react-router-dom.d.ts +++ b/packages/react/src/react-router-dom.d.ts @@ -2,6 +2,6 @@ import "react-router-dom"; declare module "react-router-dom" { export interface LinkProps { - dataBehavior?: string; // Add your custom attribute here + databehavior?: string; // Add your custom attribute here } } diff --git a/packages/react/src/routes/home/ClipsTab.tsx b/packages/react/src/routes/home/ClipsTab.tsx index 394a973f7..e5e51c57d 100644 --- a/packages/react/src/routes/home/ClipsTab.tsx +++ b/packages/react/src/routes/home/ClipsTab.tsx @@ -6,6 +6,7 @@ import { useParams } from "react-router-dom"; import { useVideoCardSizes } from "@/store/video"; import PullToRefresh from "@/components/layout/PullToRefresh"; import { useVideoFilter } from "@/hooks/useVideoFilter"; +import { ClipLanguageSelector } from "@/components/language/ClipLanguageSelector"; export function ClipsTab() { const { org } = useParams(); @@ -40,6 +41,22 @@ export function ClipsTab() { "org", ); + if (clipLangs.length === 0) + return ( +
+
No language selected
+ + +
+ ); + + if (!filteredClips.length) + return ( +
+
No clips for languages: {clipLangs.join(", ")}
+
+ ); + return ( } {tab !== "members" && } - {(user?.role === "admin" || user?.role === "editor") && ( - - )} + {(user?.role === "admin" || user?.role === "editor") && + tab != "members" && } ); } diff --git a/packages/react/src/routes/watch.tsx b/packages/react/src/routes/watch.tsx index 85c9a49a7..ec63bbf97 100644 --- a/packages/react/src/routes/watch.tsx +++ b/packages/react/src/routes/watch.tsx @@ -6,8 +6,9 @@ import { Controlbar } from "@/components/player/Controlbar"; import { Mentions } from "@/components/player/MentionsCard"; import { PlayerDescription as Description } from "@/components/player/PlayerDescription"; import { PlayerRecommendations as Recommendations } from "@/components/player/PlayerRecommendations"; -import { PlayerStats } from "@/components/player/PlayerStats"; +import { VideoStats } from "@/components/player/PlayerStats"; import { QueueList } from "@/components/player/QueueList"; +import Comments from "@/components/watch/Comments"; import { useIsLgAndUp } from "@/hooks/useBreakpoint"; import { headerHiddenAtom } from "@/hooks/useFrame"; import { cn, idToVideoURL } from "@/lib/utils"; @@ -96,6 +97,7 @@ const UnderVideoInfo = ({
+ ); }; @@ -211,7 +213,7 @@ export function Watch() {

{currentVideo?.title}

- {currentVideo && } + {currentVideo && }