@@ -2,16 +2,61 @@ import { useSocket } from "@/hooks/useSocket";
2
2
import { formatDuration } from "@/lib/time" ;
3
3
import { cn , getChannelPhoto } from "@/lib/utils" ;
4
4
import { Badge } from "@/shadcn/ui/badge" ;
5
- import { playerRefAtom } from "@/store/player" ;
5
+ import { playerRefAtom , videoStatusAtomFamily } from "@/store/player" ;
6
6
import { tldexBlockedAtom , tldexSettingsAtom } from "@/store/tldex" ;
7
7
import { useAtom , useAtomValue } from "jotai" ;
8
- import { DetailedHTMLProps , HTMLAttributes , forwardRef , useMemo } from "react" ;
9
- import { Virtuoso } from "react-virtuoso" ;
8
+ import {
9
+ DetailedHTMLProps ,
10
+ HTMLAttributes ,
11
+ forwardRef ,
12
+ useEffect ,
13
+ useMemo ,
14
+ useRef ,
15
+ } from "react" ;
16
+ import { Virtuoso , VirtuosoHandle } from "react-virtuoso" ;
10
17
import "./tlchat.css" ;
11
18
12
19
interface TLChatProps {
13
20
videoId : string ;
14
21
}
22
+ // Custom hook for timestamp indexing
23
+ function useTimestampIndex ( messages ?: ParsedMessage [ ] ) {
24
+ // Build and memoize the timestamp index
25
+ return useMemo ( ( ) => {
26
+ const findIndexForTimestamp = ( targetTimestamp : number ) => {
27
+ if ( ! messages ) return 0 ;
28
+
29
+ let left = 0 ;
30
+ let right = messages . length - 1 ;
31
+
32
+ // Handle edge cases
33
+ if ( right < 0 ) return 0 ;
34
+ if ( targetTimestamp <= messages [ 0 ] . video_offset ) return 0 ;
35
+ if ( targetTimestamp >= messages [ right ] . video_offset ) return right ;
36
+
37
+ // Binary search for closest match
38
+ while ( left <= right ) {
39
+ const mid = Math . floor ( ( left + right ) / 2 ) ;
40
+ const midTimestamp = messages [ mid ] . video_offset ;
41
+
42
+ if ( midTimestamp === targetTimestamp ) {
43
+ return mid ;
44
+ }
45
+
46
+ if ( midTimestamp < targetTimestamp ) {
47
+ left = mid + 1 ;
48
+ } else {
49
+ right = mid - 1 ;
50
+ }
51
+ }
52
+
53
+ // Find closest between the two surrounding values
54
+ return messages [ right ] . video_offset <= targetTimestamp ? right : left ;
55
+ } ;
56
+
57
+ return findIndexForTimestamp ;
58
+ } , [ messages ] ) ;
59
+ }
15
60
16
61
export function TLChat ( { videoId } : TLChatProps ) {
17
62
const tldexState = useAtomValue ( tldexSettingsAtom ) ;
@@ -20,6 +65,7 @@ export function TLChat({ videoId }: TLChatProps) {
20
65
[ videoId , tldexState . liveTlLang ] ,
21
66
) ;
22
67
const { chatDB } = useSocket ( roomID ) ;
68
+ const virtuosoRef = useRef < VirtuosoHandle > ( null ) ;
23
69
24
70
const processedMessages = useMemo ( ( ) => {
25
71
return chatDB . messages ?. map ( ( msg , i , arr ) => ( {
@@ -31,8 +77,26 @@ export function TLChat({ videoId }: TLChatProps) {
31
77
} ) ) ;
32
78
} , [ chatDB . messages ] ) ;
33
79
80
+ // inverse index of timestamp to index of message
81
+ const findIndexForTimestamp = useTimestampIndex ( processedMessages ) ;
82
+
83
+ // scroll to the video:
84
+ const videoStatusAtom = videoStatusAtomFamily ( videoId ) ;
85
+ const videoStatus = useAtomValue ( videoStatusAtom ) ;
86
+
87
+ useEffect ( ( ) => {
88
+ if ( videoStatus ?. progress === undefined || videoStatus . status !== "playing" )
89
+ return ;
90
+ const index = findIndexForTimestamp ( videoStatus . progress ) ;
91
+ virtuosoRef . current ?. scrollToIndex ( {
92
+ index : index || "LAST" ,
93
+ align : "end" ,
94
+ } ) ;
95
+ } , [ findIndexForTimestamp , videoStatus . progress , videoStatus . status ] ) ;
96
+
34
97
return (
35
98
< Virtuoso
99
+ ref = { virtuosoRef }
36
100
components = { { Item : TLChatItem } }
37
101
className = "h-full w-full bg-base-2 py-2"
38
102
initialTopMostItemIndex = { { index : "LAST" , align : "end" } }
0 commit comments