/* Review screen: the player on the left, the notes on the right. A note always carries the exact moment it was written at. Optionally it also carries a pin or a drawing on the frame — click the picture with the pin tool, or scribble with the pen. */ function CommentItem({ comment, replies, author, directory, active, onActivate, onResolve, onDelete, me, index }) { return (
onActivate(comment)}>
{author ? (author.name || author.email) : 'Someone'} {timecode(comment.timestamp_ms)}{comment.frame != null ? ` · f${comment.frame}` : ''} {comment.kind === 'client' && client} {(comment.x != null || comment.drawing) && #{index}} {(comment.author_id === me.id || me.role === 'admin') && ( )}
{comment.body}
{replies.map((r) => { const who = directory.byId[r.author_id]; return (
{who ? (who.name || who.email) : 'Someone'}
{r.body}
); })}
); } function ReviewScreen({ versionId, go, me, directory }) { const [version, setVersion] = React.useState(null); const [task, setTask] = React.useState(null); const [versions, setVersions] = React.useState([]); const [comments, setComments] = React.useState([]); const [activeId, setActiveId] = React.useState(null); const [tool, setTool] = React.useState('none'); const [draft, setDraft] = React.useState(null); const [text, setText] = React.useState(''); const [replyTo, setReplyTo] = React.useState(null); const [showResolved, setShowResolved] = React.useState(false); const [nowMs, setNowMs] = React.useState(0); const playerRef = React.useRef(null); const loadComments = React.useCallback((id) => { api.get(`/api/versions/${id}/comments`).then(setComments).catch((e) => toast(e.message, 'error')); }, []); React.useEffect(() => { setDraft(null); setTool('none'); api.get(`/api/videos/${versionId}`) .then((v) => { setVersion(v); loadComments(v.id); return api.get(`/api/tasks/${v.task_id}`); }) .then((d) => { setTask(d.task); setVersions(d.versions); }) .catch((e) => toast(e.message, 'error')); }, [versionId, loadComments]); if (!version) return
Loading…
; if (version.status !== 'ready') { return ( go('task', version.task_id)}>Back to task} /> ); } const client = task ? directory.byId[task.client_id] : null; const roots = comments.filter((c) => !c.parent_id) .filter((c) => showResolved || !c.resolved) .sort((a, b) => a.timestamp_ms - b.timestamp_ms); const repliesOf = (id) => comments.filter((c) => c.parent_id === id); const pinIndex = {}; comments.filter((c) => c.x != null || c.drawing).forEach((c, i) => { pinIndex[c.id] = i + 1; }); const startDraft = () => { const ms = playerRef.current ? playerRef.current.currentMs() : nowMs; playerRef.current && playerRef.current.pause(); setDraft((d) => d || { timestamp_ms: ms, frame: frameAt(ms, version.fps || 25) }); }; const onAnnotate = (payload) => { const ms = playerRef.current ? playerRef.current.currentMs() : nowMs; setDraft((d) => { const base = d || { timestamp_ms: ms, frame: frameAt(ms, version.fps || 25) }; if (payload.pin) return { ...base, x: payload.pin.x, y: payload.pin.y }; if (payload.stroke) { const strokes = (base.drawing && base.drawing.strokes) || []; return { ...base, drawing: { strokes: [...strokes, payload.stroke] } }; } return base; }); }; const submit = () => { if (!text.trim()) return; const ms = draft ? draft.timestamp_ms : (playerRef.current ? playerRef.current.currentMs() : nowMs); const payload = { body: text, timestamp_ms: replyTo ? replyTo.timestamp_ms : ms, frame: draft ? draft.frame : frameAt(ms, version.fps || 25), x: draft ? draft.x : null, y: draft ? draft.y : null, drawing: draft ? draft.drawing : null, parent_id: replyTo ? replyTo.id : null, }; api.post(`/api/versions/${version.id}/comments`, payload) .then(() => { setText(''); setDraft(null); setReplyTo(null); setTool('none'); loadComments(version.id); }) .catch((e) => toast(e.message, 'error')); }; const activate = (comment) => { setActiveId(comment.id); if (playerRef.current) playerRef.current.seekMs(comment.timestamp_ms); }; return ( <>
{client && {client.name}} {task ? task.title : ''} {version.label && {version.label}} {task && } {versions.length > 1 && ( )}
setNowMs(ms)} />
Notes {roots.length}
{roots.length === 0 && (

No notes yet. Pause where something is wrong and write one.

)} {roots.map((c) => ( api.patch(`/api/comments/${cm.id}`, { resolved: !cm.resolved }) .then(() => loadComments(version.id)).catch((e) => toast(e.message, 'error'))} onDelete={(cm) => api.del(`/api/comments/${cm.id}`) .then(() => loadComments(version.id)).catch((e) => toast(e.message, 'error'))} /> ))}
{replyTo && (
Replying at {timecode(replyTo.timestamp_ms)}
)}
{draft ? `at ${timecode(draft.timestamp_ms, version.fps)}` : `at ${timecode(nowMs, version.fps)}`} {draft && (draft.x != null || draft.drawing) && ( )}