Skip to content

Commit 1155f49

Browse files
committed
add comments to watch page.
1 parent 320d766 commit 1155f49

File tree

8 files changed

+152
-10
lines changed

8 files changed

+152
-10
lines changed

packages/react/src/components/player/PlayerStats.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { localeAtom } from "@/store/i18n";
77
import { useAtomValue } from "jotai";
88
import { useTranslation } from "react-i18next";
99

10-
export function PlayerStats({
10+
export function VideoStats({
1111
type,
1212
status,
1313
topic_id,

packages/react/src/components/video/VideoCard.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ export function VideoCard({
217217
{(size == "lg" || size == "md") && video.channel && (
218218
<Link
219219
to={`/channel/${video.channel.id}`}
220-
dataBehavior="channelLink"
220+
databehavior="channelLink"
221221
className="shrink-0"
222222
onClick={(e) =>
223223
onClick ? onClick("channel", video, e) : goToVideoClickHandler(e)
@@ -258,7 +258,7 @@ export function VideoCard({
258258
{video.channel && (
259259
<Link
260260
className={videoCardClasses.channelLink}
261-
dataBehavior="channelLink"
261+
databehavior="channelLink"
262262
to={`/channel/${video.channel.id}`}
263263
onClick={(e) => onClick && onClick("channel", video, e)}
264264
>

packages/react/src/components/video/VideoCard.utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function useDefaultVideoCardClickHandler(
2929
// thumbnail, title, and channel image / channel name are all React Router links
3030
// in contrast, the rest of the video card is not a link
3131
const isChannelClick =
32-
isLinkClick?.getAttribute("dataBehavior") === "channelLink";
32+
isLinkClick?.getAttribute("databehavior") === "channelLink";
3333

3434
if (isChannelClick) {
3535
return; // do not select, skip the entire handler
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import React, { useState, useMemo, useRef, useEffect } from "react";
2+
import { ExternalLink } from "lucide-react";
3+
import { videoPlayerRefAtomFamily } from "@/store/player";
4+
import { useAtomValue } from "jotai";
5+
6+
interface CommentData {
7+
message: string;
8+
comment_key: string;
9+
}
10+
11+
interface TruncatedTextProps {
12+
text: string;
13+
}
14+
15+
const TruncatedText = ({ text }: TruncatedTextProps) => {
16+
const contentRef = useRef<HTMLDivElement>(null);
17+
const [isClamped, setClamped] = useState(false);
18+
19+
useEffect(() => {
20+
if (contentRef && contentRef.current) {
21+
setClamped(
22+
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.
23+
);
24+
}
25+
}, []);
26+
27+
const [expanded, setExpanded] = useState(false);
28+
return (
29+
<div>
30+
<div
31+
ref={contentRef}
32+
className={`whitespace-pre-wrap break-words ${!expanded ? `line-clamp-5` : ""}`}
33+
>
34+
<span dangerouslySetInnerHTML={{ __html: text }} />
35+
</div>
36+
{isClamped && (
37+
<button
38+
onClick={() => setExpanded(!expanded)}
39+
className="mt-1 text-xs text-base-11 hover:text-base-12"
40+
>
41+
{expanded ? "Show less" : "Read more"}
42+
</button>
43+
)}
44+
</div>
45+
);
46+
};
47+
48+
const parseTimestamps = (message: string, videoId: string) => {
49+
const decoder = document.createElement("div");
50+
decoder.innerHTML = message;
51+
const sanitizedText = decoder.textContent || "";
52+
53+
return sanitizedText.replace(
54+
/(?:([0-5]?[0-9]):)?([0-5]?[0-9]):([0-5][0-9])/gm,
55+
(match, hr, min, sec) => {
56+
const seconds = Number(hr ?? 0) * 3600 + Number(min) * 60 + Number(sec);
57+
return `<a href="/watch/${videoId}?t=${seconds}" data-time="${seconds}" class="inline-block rounded px-1 text-primary-11 hover:bg-primary hover:text-primary-12">${match}</a>`;
58+
},
59+
);
60+
};
61+
62+
const Comment = ({
63+
comment,
64+
videoId,
65+
}: {
66+
comment: CommentData;
67+
videoId: string;
68+
}) => {
69+
const parsedMessage = useMemo(
70+
() => parseTimestamps(comment.message, videoId),
71+
[comment.message, videoId],
72+
);
73+
74+
return (
75+
<div className="group relative my-3 min-h-0 border-l-2 border-base-6 px-4 py-1">
76+
<TruncatedText text={parsedMessage} />
77+
<a
78+
href={`https://www.youtube.com/watch?v=${videoId}&lc=${comment.comment_key}`}
79+
target="_blank"
80+
rel="noopener noreferrer"
81+
className="absolute right-0 top-0 hidden text-base-11 hover:text-base-12 group-hover:block"
82+
>
83+
<ExternalLink className="h-4 w-4" />
84+
</a>
85+
</div>
86+
);
87+
};
88+
89+
export const Comments = ({ video }: { video?: PlaceholderVideo }) => {
90+
const playerRefAtom = videoPlayerRefAtomFamily(
91+
video?.id || "__nonexistent__",
92+
);
93+
const player = useAtomValue(playerRefAtom);
94+
const goToTimestampHandler = (e: React.MouseEvent<HTMLAnchorElement>) => {
95+
if (e.target instanceof HTMLAnchorElement) {
96+
if (e.target.dataset.time) {
97+
console.log(e.target.dataset.time, player);
98+
player?.seekTo(+e.target.dataset.time, "seconds");
99+
player?.getInternalPlayer().playVideo?.();
100+
e.preventDefault();
101+
}
102+
}
103+
};
104+
105+
if (!video?.comments?.length) return null;
106+
return (
107+
<div
108+
className="space-y-2"
109+
onClick={
110+
goToTimestampHandler as unknown as React.MouseEventHandler<HTMLDivElement>
111+
}
112+
>
113+
{video.comments.map((comment) => (
114+
<Comment
115+
key={comment.comment_key}
116+
comment={comment}
117+
videoId={video.id}
118+
/>
119+
))}
120+
</div>
121+
);
122+
};
123+
124+
export default Comments;

packages/react/src/react-router-dom.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ import "react-router-dom";
22

33
declare module "react-router-dom" {
44
export interface LinkProps {
5-
dataBehavior?: string; // Add your custom attribute here
5+
databehavior?: string; // Add your custom attribute here
66
}
77
}

packages/react/src/routes/home/ClipsTab.tsx

+17
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useParams } from "react-router-dom";
66
import { useVideoCardSizes } from "@/store/video";
77
import PullToRefresh from "@/components/layout/PullToRefresh";
88
import { useVideoFilter } from "@/hooks/useVideoFilter";
9+
import { ClipLanguageSelector } from "@/components/language/ClipLanguageSelector";
910

1011
export function ClipsTab() {
1112
const { org } = useParams();
@@ -40,6 +41,22 @@ export function ClipsTab() {
4041
"org",
4142
);
4243

44+
if (clipLangs.length === 0)
45+
return (
46+
<div className="gap-4 px-4 py-2 @container md:px-8">
47+
<div>No language selected</div>
48+
49+
<ClipLanguageSelector />
50+
</div>
51+
);
52+
53+
if (!filteredClips.length)
54+
return (
55+
<div className="gap-4 px-4 py-2 @container md:px-8">
56+
<div>No clips for languages: {clipLangs.join(", ")}</div>
57+
</div>
58+
);
59+
4360
return (
4461
<PullToRefresh onRefresh={refetch}>
4562
<MainVideoListing

packages/react/src/routes/home/home.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,8 @@ function StickyTabsList({
132132
{/* Optional Control Buttons */}
133133
{tab === "clips" && <ClipLanguageSelector />}
134134
{tab !== "members" && <CardSizeToggle />}
135-
{(user?.role === "admin" || user?.role === "editor") && (
136-
<EditingStateToggle />
137-
)}
135+
{(user?.role === "admin" || user?.role === "editor") &&
136+
tab != "members" && <EditingStateToggle />}
138137
</TabsList>
139138
);
140139
}

packages/react/src/routes/watch.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { Controlbar } from "@/components/player/Controlbar";
66
import { Mentions } from "@/components/player/MentionsCard";
77
import { PlayerDescription as Description } from "@/components/player/PlayerDescription";
88
import { PlayerRecommendations as Recommendations } from "@/components/player/PlayerRecommendations";
9-
import { PlayerStats } from "@/components/player/PlayerStats";
9+
import { VideoStats } from "@/components/player/PlayerStats";
1010
import { QueueList } from "@/components/player/QueueList";
11+
import Comments from "@/components/watch/Comments";
1112
import { useIsLgAndUp } from "@/hooks/useBreakpoint";
1213
import { headerHiddenAtom } from "@/hooks/useFrame";
1314
import { cn, idToVideoURL } from "@/lib/utils";
@@ -96,6 +97,7 @@ const UnderVideoInfo = ({
9697
<div className="flex @screen-lg:hidden">
9798
<Recommendations {...currentVideo} />
9899
</div>
100+
<Comments video={currentVideo} />
99101
</div>
100102
);
101103
};
@@ -211,7 +213,7 @@ export function Watch() {
211213

212214
<div className={titleSectionClasses}>
213215
<h2 className="text-xl font-bold">{currentVideo?.title}</h2>
214-
{currentVideo && <PlayerStats {...currentVideo} />}
216+
{currentVideo && <VideoStats {...currentVideo} />}
215217
</div>
216218

217219
<UnderVideoInfo currentVideo={currentVideo} channel={channel} />

0 commit comments

Comments
 (0)