jbilcke-hf HF staff commited on
Commit
bde82a3
1 Parent(s): 664b6fe

add support for time seeks

Browse files
src/app/interface/comment-card/index.tsx CHANGED
@@ -3,6 +3,7 @@ import { CommentInfo } from "@/types"
3
  import { useEffect, useState } from "react"
4
  import { DefaultAvatar } from "../default-avatar"
5
  import { formatTimeAgo } from "@/lib/formatTimeAgo"
 
6
 
7
  export function CommentCard({
8
  comment,
@@ -34,7 +35,6 @@ export function CommentCard({
34
  }
35
  }
36
 
37
-
38
  return (
39
  <div className={cn(
40
  `flex flex-col w-full`,
@@ -77,14 +77,26 @@ export function CommentCard({
77
  <div className="text-xs font-medium text-zinc-100">@{comment?.userInfo?.userName}</div>
78
  <div className="text-xs font-medium text-neutral-400">{formatTimeAgo(comment.updatedAt)}</div>
79
  </div>
80
- <p className={cn(
81
- `text-sm font-normal`,
82
- `shrink`,
83
- `overflow-hidden break-words`,
84
- isExpanded ? `` : `line-clamp-4`
85
- )}>{
86
- comment.message
87
- }</p>
 
 
 
 
 
 
 
 
 
 
 
 
88
  {isLongContent &&
89
  <div className={cn(
90
  `flex`,
 
3
  import { useEffect, useState } from "react"
4
  import { DefaultAvatar } from "../default-avatar"
5
  import { formatTimeAgo } from "@/lib/formatTimeAgo"
6
+ import { CommentWithTimeSeeks } from "./time-seeker"
7
 
8
  export function CommentCard({
9
  comment,
 
35
  }
36
  }
37
 
 
38
  return (
39
  <div className={cn(
40
  `flex flex-col w-full`,
 
77
  <div className="text-xs font-medium text-zinc-100">@{comment?.userInfo?.userName}</div>
78
  <div className="text-xs font-medium text-neutral-400">{formatTimeAgo(comment.updatedAt)}</div>
79
  </div>
80
+ <CommentWithTimeSeeks
81
+ className={cn(
82
+ `text-sm font-normal`,
83
+ `shrink`,
84
+ `overflow-hidden break-words`,
85
+ isExpanded ? `` : `line-clamp-4`
86
+ )}
87
+ linkClassName="font-medium text-neutral-400 cursor-pointer hover:underline"
88
+ onSeek={(timeInSec) => {
89
+ try {
90
+ const videoElement: HTMLVideoElement | undefined = (document.getElementsByClassName("tuby-container")?.[0]?.children?.[0] as any)
91
+ if (videoElement) {
92
+ videoElement.currentTime = timeInSec
93
+ }
94
+ } catch (err) {
95
+ //
96
+ }
97
+ }}>{
98
+ comment.message
99
+ }</CommentWithTimeSeeks>
100
  {isLongContent &&
101
  <div className={cn(
102
  `flex`,
src/app/interface/comment-card/time-seeker.tsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react"
2
+
3
+ interface CommentWithTimeSeeksProps {
4
+ onSeek: (timeInSec: number) => void;
5
+ children: string;
6
+ className?: string;
7
+ linkClassName?: string;
8
+ }
9
+
10
+ export const CommentWithTimeSeeks: React.FC<CommentWithTimeSeeksProps> = ({ onSeek, children, className, linkClassName }) => {
11
+ // This function converts a time string like "HH:MM:SS", "MM:SS" or "SS" to seconds.
12
+ const convertTimeToSeconds = (timeString: string): number => {
13
+ const units = timeString.split(":").map(unit => parseInt(unit, 10));
14
+ const seconds = units.reverse().reduce((acc, unit, index) => acc + unit * Math.pow(60, index), 0);
15
+ return seconds;
16
+ };
17
+
18
+ // This function parses the text and replaces time seeks with clickable spans.
19
+ const renderWithTimeSeek = (text: string) => {
20
+ const timeSeekPattern = /\b(\d{1,2}:)?\d{1,2}:\d{2}\b/g;
21
+ const nodes = [];
22
+ let lastIndex = 0;
23
+
24
+ text.replace(timeSeekPattern, (match, ...args) => {
25
+ const index = args[args.length - 2]; // The second to last argument is the index of the match
26
+ nodes.push(
27
+ <React.Fragment key={lastIndex}>
28
+ {text.slice(lastIndex, index)}{/* Text before the match */}
29
+ <span
30
+ className={linkClassName}
31
+ onClick={() => onSeek(convertTimeToSeconds(match))}
32
+ >
33
+ {match}
34
+ </span>
35
+ </React.Fragment>
36
+ );
37
+ lastIndex = index + match.length; // Update lastIndex to end of the current match
38
+ return match; // return value is unused but needed for `replace` to work
39
+ });
40
+
41
+ // Append the remaining text after the last match (if any)
42
+ if (lastIndex < text.length) {
43
+ nodes.push(<React.Fragment key={lastIndex}>{text.slice(lastIndex)}</React.Fragment>);
44
+ }
45
+
46
+ return nodes;
47
+ };
48
+
49
+ return <p className={className}>{renderWithTimeSeek(children)}</p>;
50
+ };
src/app/interface/video-player/index.tsx CHANGED
@@ -5,17 +5,33 @@ import "react-tuby/css/main.css"
5
 
6
  import { cn } from "@/lib/utils"
7
  import { VideoInfo } from "@/types"
 
 
8
 
9
  export function VideoPlayer({
10
  video,
11
  enableShortcuts = true,
12
- className = ""
 
13
  }: {
14
  video?: VideoInfo
15
  enableShortcuts?: boolean
16
  className?: string
 
17
  }) {
18
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  // TODO: keep the same form factor?
20
  if (!video) { return null }
21
 
@@ -33,6 +49,8 @@ export function VideoPlayer({
33
  )}>
34
  <Player
35
 
 
 
36
  src={[
37
  {
38
  quality: "Full HD",
 
5
 
6
  import { cn } from "@/lib/utils"
7
  import { VideoInfo } from "@/types"
8
+ import { MutableRefObject, useEffect, useRef } from "react"
9
+ import { isValidNumber } from "@/app/server/actions/utils/isValidNumber"
10
 
11
  export function VideoPlayer({
12
  video,
13
  enableShortcuts = true,
14
+ className = "",
15
+ // currentTime,
16
  }: {
17
  video?: VideoInfo
18
  enableShortcuts?: boolean
19
  className?: string
20
+ // currentTime?: number
21
  }) {
22
 
23
+ /*
24
+ const ref = useRef(null)
25
+
26
+ useEffect(() => {
27
+ if (!ref.current) { return }
28
+ if (!isValidNumber(currentTime)) { return }
29
+
30
+ (ref.current as any).currentTime = currentTime
31
+ // $(".tuby-container video").currentTime = 2
32
+ }, [currentTime])
33
+ */
34
+
35
  // TODO: keep the same form factor?
36
  if (!video) { return null }
37
 
 
49
  )}>
50
  <Player
51
 
52
+ // playerRef={ref}
53
+
54
  src={[
55
  {
56
  quality: "Full HD",
src/app/views/public-video-view/index.tsx CHANGED
@@ -1,6 +1,6 @@
1
  "use client"
2
 
3
- import { useEffect, useState, useTransition } from "react"
4
  import { RiCheckboxCircleFill } from "react-icons/ri"
5
  import { PiShareFatLight } from "react-icons/pi"
6
  import CopyToClipboard from "react-copy-to-clipboard"
@@ -37,9 +37,14 @@ export function PublicVideoView() {
37
  const [isCommenting, setCommenting] = useState(false)
38
  const [isFocusedOnInput, setFocusedOnInput] = useState(false)
39
 
 
 
 
 
 
40
  const currentUser = useCurrentUser()
41
 
42
- const [userThumbnail, setUserThumbnail] = useState("")
43
 
44
  useEffect(() => {
45
  setUserThumbnail(currentUser?.thumbnail || "")
@@ -52,6 +57,7 @@ export function PublicVideoView() {
52
  }
53
  }
54
 
 
55
  const video = useStore(s => s.publicVideo)
56
 
57
  const videoId = `${video?.id || ""}`
@@ -166,6 +172,10 @@ export function PublicVideoView() {
166
  <VideoPlayer
167
  video={video}
168
  enableShortcuts={!isFocusedOnInput}
 
 
 
 
169
  className="mb-4"
170
  />
171
 
 
1
  "use client"
2
 
3
+ import { useEffect, useRef, useState, useTransition } from "react"
4
  import { RiCheckboxCircleFill } from "react-icons/ri"
5
  import { PiShareFatLight } from "react-icons/pi"
6
  import CopyToClipboard from "react-copy-to-clipboard"
 
37
  const [isCommenting, setCommenting] = useState(false)
38
  const [isFocusedOnInput, setFocusedOnInput] = useState(false)
39
 
40
+ // current time in the video
41
+ // note: this is used to *set* the current time, not to read it
42
+ // EDIT: you know what, let's do this the dirty way for now
43
+ // const [desiredCurrentTime, setDesiredCurrentTime] = useState()
44
+
45
  const currentUser = useCurrentUser()
46
 
47
+ const [userThumbnail, setUserThumbnail] = useState("")
48
 
49
  useEffect(() => {
50
  setUserThumbnail(currentUser?.thumbnail || "")
 
57
  }
58
  }
59
 
60
+
61
  const video = useStore(s => s.publicVideo)
62
 
63
  const videoId = `${video?.id || ""}`
 
172
  <VideoPlayer
173
  video={video}
174
  enableShortcuts={!isFocusedOnInput}
175
+
176
+ // that could be, but let's do it the dirty way for now
177
+ // currentTime={desiredCurrentTime}
178
+
179
  className="mb-4"
180
  />
181