Skip to content

Commit 8070dbf

Browse files
committed
TL chat autoscroll
1 parent 5ab71e2 commit 8070dbf

File tree

1 file changed

+67
-3
lines changed

1 file changed

+67
-3
lines changed

packages/react/src/components/tldex/TLChat.tsx

+67-3
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,61 @@ import { useSocket } from "@/hooks/useSocket";
22
import { formatDuration } from "@/lib/time";
33
import { cn, getChannelPhoto } from "@/lib/utils";
44
import { Badge } from "@/shadcn/ui/badge";
5-
import { playerRefAtom } from "@/store/player";
5+
import { playerRefAtom, videoStatusAtomFamily } from "@/store/player";
66
import { tldexBlockedAtom, tldexSettingsAtom } from "@/store/tldex";
77
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";
1017
import "./tlchat.css";
1118

1219
interface TLChatProps {
1320
videoId: string;
1421
}
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+
}
1560

1661
export function TLChat({ videoId }: TLChatProps) {
1762
const tldexState = useAtomValue(tldexSettingsAtom);
@@ -20,6 +65,7 @@ export function TLChat({ videoId }: TLChatProps) {
2065
[videoId, tldexState.liveTlLang],
2166
);
2267
const { chatDB } = useSocket(roomID);
68+
const virtuosoRef = useRef<VirtuosoHandle>(null);
2369

2470
const processedMessages = useMemo(() => {
2571
return chatDB.messages?.map((msg, i, arr) => ({
@@ -31,8 +77,26 @@ export function TLChat({ videoId }: TLChatProps) {
3177
}));
3278
}, [chatDB.messages]);
3379

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+
3497
return (
3598
<Virtuoso
99+
ref={virtuosoRef}
36100
components={{ Item: TLChatItem }}
37101
className="h-full w-full bg-base-2 py-2"
38102
initialTopMostItemIndex={{ index: "LAST", align: "end" }}

0 commit comments

Comments
 (0)