feat: enhance comment functionality with timestamp range support

- Added timestampEnd to Comment and CommentReply interfaces.
- Implemented logic for handling comment timestamp ranges in the comment composer and comments pane.
- Updated video player and player core to support frame stepping and improved seeking functionality.
- Introduced frame mode toggle for precise navigation during video playback.
- Closes #12
This commit is contained in:
yusufipk
2026-04-25 22:58:16 +03:00
parent 63f331d220
commit 378ca1977b
8 changed files with 537 additions and 110 deletions
+90 -2
View File
@@ -58,7 +58,7 @@ interface CommentsPaneProps {
handleSeekToTimestamp: (
timestamp: number,
annotation?: string | null,
options?: { pauseAfterSeek?: boolean }
options?: { pauseAfterSeek?: boolean; timestampEnd?: number | null }
) => void;
currentUserId: string | null;
projectOwnerId: string;
@@ -87,6 +87,10 @@ interface CommentsPaneProps {
setReplyingTo: (id: string | null) => void;
replyText: string;
setReplyText: (value: string) => void;
replyRangeStart: number | null;
replyRangeEnd: number | null;
toggleReplyRangeSelection: () => void;
clearReplyRangeSelection: () => void;
handleReplyComment: (
parentId: string,
voiceData?: { url: string; duration: number },
@@ -161,6 +165,10 @@ export const CommentsPane = memo(function CommentsPane({
setReplyingTo,
replyText,
setReplyText,
replyRangeStart,
replyRangeEnd,
toggleReplyRangeSelection,
clearReplyRangeSelection,
handleReplyComment,
startReplyRecording,
isReplyRecording,
@@ -186,6 +194,18 @@ export const CommentsPane = memo(function CommentsPane({
assetsPane,
}: CommentsPaneProps) {
const [isPaneDraggingOver, setIsPaneDraggingOver] = useState(false);
const formatCommentRange = (timestamp: number, timestampEnd: number | null) => {
if (timestampEnd === null) return formatTime(timestamp);
return `${formatTime(timestamp)} - ${formatTime(timestampEnd)}`;
};
const replyRangeButtonLabel =
replyRangeStart === null || replyRangeEnd !== null ? 'Set In' : 'Set Out';
const replyRangeLabel =
replyRangeStart !== null
? replyRangeEnd !== null
? `${formatTime(replyRangeStart)} - ${formatTime(replyRangeEnd)}`
: `In ${formatTime(replyRangeStart)}`
: null;
return (
<>
@@ -383,13 +403,14 @@ export const CommentsPane = memo(function CommentsPane({
onClick={() =>
handleSeekToTimestamp(comment.timestamp, comment.annotationData, {
pauseAfterSeek: true,
timestampEnd: comment.timestampEnd,
})
}
className="flex items-center gap-1 text-xs text-primary hover:underline px-1.5 py-0.5 rounded bg-primary/10 hover:bg-primary/20 transition-colors"
title="Jump to this timestamp"
>
<Clock className="h-3 w-3" />
{formatTime(comment.timestamp)}
{formatCommentRange(comment.timestamp, comment.timestampEnd)}
<ArrowUpRight className="h-3 w-3" />
</button>
{canResolveComments && (
@@ -416,6 +437,7 @@ export const CommentsPane = memo(function CommentsPane({
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
clearReplyRangeSelection();
setReplyingTo(comment.id);
setReplyText('');
}}
@@ -670,6 +692,19 @@ export const CommentsPane = memo(function CommentsPane({
</AvatarFallback>
</Avatar>
<span className="font-medium text-xs">{replyAuthor}</span>
<button
onClick={() =>
handleSeekToTimestamp(reply.timestamp, reply.annotationData, {
pauseAfterSeek: true,
timestampEnd: reply.timestampEnd,
})
}
className="flex items-center gap-1 rounded bg-primary/10 px-1.5 py-0.5 text-[10px] text-primary transition-colors hover:bg-primary/20"
title="Jump to this reply"
>
<Clock className="h-2.5 w-2.5" />
{formatCommentRange(reply.timestamp, reply.timestampEnd)}
</button>
<span className="text-xs text-muted-foreground">
{new Date(reply.createdAt).toLocaleDateString()}
</span>
@@ -938,6 +973,31 @@ export const CommentsPane = memo(function CommentsPane({
rows={1}
className="resize-none text-sm"
/>
<div className="flex items-center gap-2 flex-wrap">
<Button
size="sm"
variant={replyRangeStart !== null ? 'default' : 'outline'}
className="h-7 text-xs"
onClick={toggleReplyRangeSelection}
>
{replyRangeButtonLabel}
</Button>
{replyRangeLabel && (
<span className="rounded-md border px-2 py-1 text-xs text-muted-foreground tabular-nums">
{replyRangeLabel}
</span>
)}
{replyRangeStart !== null && (
<Button
size="sm"
variant="ghost"
className="h-7 text-xs"
onClick={clearReplyRangeSelection}
>
Clear
</Button>
)}
</div>
<div className="flex gap-1 mt-2">
<Button
size="sm"
@@ -1001,6 +1061,7 @@ export const CommentsPane = memo(function CommentsPane({
handleReplyComment(comment.id);
}
if (e.key === 'Escape') {
clearReplyRangeSelection();
setReplyingTo(null);
setReplyText('');
}
@@ -1033,6 +1094,31 @@ export const CommentsPane = memo(function CommentsPane({
onChange={(e) => handleImageSelect(e, true)}
/>
</div>
<div className="mt-2 flex items-center gap-2 flex-wrap">
<Button
size="sm"
variant={replyRangeStart !== null ? 'default' : 'outline'}
className="h-7 text-xs"
onClick={toggleReplyRangeSelection}
>
{replyRangeButtonLabel}
</Button>
{replyRangeLabel && (
<span className="rounded-md border px-2 py-1 text-xs text-muted-foreground tabular-nums">
{replyRangeLabel}
</span>
)}
{replyRangeStart !== null && (
<Button
size="sm"
variant="ghost"
className="h-7 text-xs"
onClick={clearReplyRangeSelection}
>
Clear
</Button>
)}
</div>
<div className="flex gap-1 mt-1">
<Button
size="sm"
@@ -1054,6 +1140,7 @@ export const CommentsPane = memo(function CommentsPane({
size="sm"
variant="ghost"
onClick={() => {
clearReplyRangeSelection();
setReplyingTo(null);
setReplyText('');
}}
@@ -1070,6 +1157,7 @@ export const CommentsPane = memo(function CommentsPane({
{!isReplying && !isEditing && (
<button
onClick={() => {
clearReplyRangeSelection();
setReplyingTo(comment.id);
setReplyText('');
}}