mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
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:
@@ -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('');
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user