mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
refactor: Implement optimistic UI updates for comment handling with mutation tracking
This commit is contained in:
@@ -179,6 +179,7 @@ export default function VideoPage() {
|
|||||||
const [editText, setEditText] = useState('');
|
const [editText, setEditText] = useState('');
|
||||||
const [isSubmittingEdit, setIsSubmittingEdit] = useState(false);
|
const [isSubmittingEdit, setIsSubmittingEdit] = useState(false);
|
||||||
const [deletingCommentId, setDeletingCommentId] = useState<string | null>(null);
|
const [deletingCommentId, setDeletingCommentId] = useState<string | null>(null);
|
||||||
|
const isMutatingRef = useRef(false);
|
||||||
|
|
||||||
// Guest name (for unauthenticated users on public projects)
|
// Guest name (for unauthenticated users on public projects)
|
||||||
const [guestName, setGuestName] = useState('');
|
const [guestName, setGuestName] = useState('');
|
||||||
@@ -568,6 +569,7 @@ export default function VideoPage() {
|
|||||||
setAudioBlob(null);
|
setAudioBlob(null);
|
||||||
|
|
||||||
setIsSubmittingComment(true);
|
setIsSubmittingComment(true);
|
||||||
|
isMutatingRef.current = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/versions/${activeVersion.id}/comments`, {
|
const res = await fetch(`/api/versions/${activeVersion.id}/comments`, {
|
||||||
@@ -628,6 +630,7 @@ export default function VideoPage() {
|
|||||||
toast.error('Failed to add comment');
|
toast.error('Failed to add comment');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmittingComment(false);
|
setIsSubmittingComment(false);
|
||||||
|
isMutatingRef.current = false;
|
||||||
}
|
}
|
||||||
}, [commentText, currentTime, selectedTimestamp, activeVersion, activeVersionId, isGuest, guestName, selectedTagId, availableTags]);
|
}, [commentText, currentTime, selectedTimestamp, activeVersion, activeVersionId, isGuest, guestName, selectedTagId, availableTags]);
|
||||||
|
|
||||||
@@ -820,6 +823,7 @@ export default function VideoPage() {
|
|||||||
|
|
||||||
const handleResolveComment = useCallback(
|
const handleResolveComment = useCallback(
|
||||||
async (commentId: string, currentlyResolved: boolean) => {
|
async (commentId: string, currentlyResolved: boolean) => {
|
||||||
|
isMutatingRef.current = true;
|
||||||
// Optimistically toggle
|
// Optimistically toggle
|
||||||
setVideo((prev) => {
|
setVideo((prev) => {
|
||||||
if (!prev) return prev;
|
if (!prev) return prev;
|
||||||
@@ -884,6 +888,8 @@ export default function VideoPage() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
toast.error('Failed to update comment');
|
toast.error('Failed to update comment');
|
||||||
|
} finally {
|
||||||
|
isMutatingRef.current = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeVersionId]
|
[activeVersionId]
|
||||||
@@ -934,6 +940,7 @@ export default function VideoPage() {
|
|||||||
setReplyRecordingTime(0);
|
setReplyRecordingTime(0);
|
||||||
|
|
||||||
setIsSubmittingReply(true);
|
setIsSubmittingReply(true);
|
||||||
|
isMutatingRef.current = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/versions/${activeVersion.id}/comments`, {
|
const res = await fetch(`/api/versions/${activeVersion.id}/comments`, {
|
||||||
@@ -1015,6 +1022,7 @@ export default function VideoPage() {
|
|||||||
toast.error('Failed to add reply');
|
toast.error('Failed to add reply');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmittingReply(false);
|
setIsSubmittingReply(false);
|
||||||
|
isMutatingRef.current = false;
|
||||||
}
|
}
|
||||||
}, [replyText, activeVersion, activeVersionId, comments, currentTime, isGuest, guestName]);
|
}, [replyText, activeVersion, activeVersionId, comments, currentTime, isGuest, guestName]);
|
||||||
|
|
||||||
@@ -1090,6 +1098,7 @@ export default function VideoPage() {
|
|||||||
const handleEditComment = useCallback(async (commentId: string) => {
|
const handleEditComment = useCallback(async (commentId: string) => {
|
||||||
if (!editText.trim()) return;
|
if (!editText.trim()) return;
|
||||||
setIsSubmittingEdit(true);
|
setIsSubmittingEdit(true);
|
||||||
|
isMutatingRef.current = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/comments/${commentId}`, {
|
const res = await fetch(`/api/comments/${commentId}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@@ -1126,12 +1135,14 @@ export default function VideoPage() {
|
|||||||
console.error('Failed to edit comment:', err);
|
console.error('Failed to edit comment:', err);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmittingEdit(false);
|
setIsSubmittingEdit(false);
|
||||||
|
isMutatingRef.current = false;
|
||||||
}
|
}
|
||||||
}, [editText, activeVersionId]);
|
}, [editText, activeVersionId]);
|
||||||
|
|
||||||
// Delete a comment
|
// Delete a comment
|
||||||
const handleDeleteComment = useCallback(async (commentId: string) => {
|
const handleDeleteComment = useCallback(async (commentId: string) => {
|
||||||
setDeletingCommentId(commentId);
|
setDeletingCommentId(commentId);
|
||||||
|
isMutatingRef.current = true;
|
||||||
|
|
||||||
// Optimistically remove from UI
|
// Optimistically remove from UI
|
||||||
const previousVideo = video;
|
const previousVideo = video;
|
||||||
@@ -1165,6 +1176,7 @@ export default function VideoPage() {
|
|||||||
setVideo(previousVideo);
|
setVideo(previousVideo);
|
||||||
} finally {
|
} finally {
|
||||||
setDeletingCommentId(null);
|
setDeletingCommentId(null);
|
||||||
|
isMutatingRef.current = false;
|
||||||
}
|
}
|
||||||
}, [activeVersionId, video]);
|
}, [activeVersionId, video]);
|
||||||
|
|
||||||
@@ -1173,18 +1185,15 @@ export default function VideoPage() {
|
|||||||
if (!activeVersion) return;
|
if (!activeVersion) return;
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const hasPendingComments = activeVersion.comments.some(
|
if (isMutatingRef.current) return;
|
||||||
(c) => c.id.startsWith('temp-')
|
|
||||||
) || activeVersion.comments.some(
|
|
||||||
(c) => c.replies.some((r) => r.id.startsWith('temp-'))
|
|
||||||
);
|
|
||||||
if (hasPendingComments) return;
|
|
||||||
|
|
||||||
const res = await fetch(`/api/projects/${projectId}/videos/${videoId}`);
|
const res = await fetch(`/api/projects/${projectId}/videos/${videoId}`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
if (!isMutatingRef.current) {
|
||||||
setVideo(data.data);
|
setVideo(data.data);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch { /* silent */ }
|
} catch { /* silent */ }
|
||||||
}, 10000);
|
}, 10000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ export default function WatchPage() {
|
|||||||
const audioPlayerRef = useRef<HTMLAudioElement | null>(null);
|
const audioPlayerRef = useRef<HTMLAudioElement | null>(null);
|
||||||
const voiceRafRef = useRef<number | null>(null);
|
const voiceRafRef = useRef<number | null>(null);
|
||||||
const voiceKnownDurationRef = useRef<number>(0);
|
const voiceKnownDurationRef = useRef<number>(0);
|
||||||
|
const isMutatingRef = useRef(false);
|
||||||
const [selectedTimestamp, setSelectedTimestamp] = useState<number | null>(null);
|
const [selectedTimestamp, setSelectedTimestamp] = useState<number | null>(null);
|
||||||
const [showResolved, setShowResolved] = useState(false);
|
const [showResolved, setShowResolved] = useState(false);
|
||||||
|
|
||||||
@@ -524,6 +525,7 @@ export default function WatchPage() {
|
|||||||
setAudioBlob(null);
|
setAudioBlob(null);
|
||||||
|
|
||||||
setIsSubmittingComment(true);
|
setIsSubmittingComment(true);
|
||||||
|
isMutatingRef.current = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/versions/${activeVersion.id}/comments`, {
|
const res = await fetch(`/api/versions/${activeVersion.id}/comments`, {
|
||||||
@@ -584,6 +586,7 @@ export default function WatchPage() {
|
|||||||
toast.error('Failed to add comment');
|
toast.error('Failed to add comment');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmittingComment(false);
|
setIsSubmittingComment(false);
|
||||||
|
isMutatingRef.current = false;
|
||||||
}
|
}
|
||||||
}, [commentText, currentTime, selectedTimestamp, activeVersion, activeVersionId, isGuest, guestName, selectedTagId, availableTags]);
|
}, [commentText, currentTime, selectedTimestamp, activeVersion, activeVersionId, isGuest, guestName, selectedTagId, availableTags]);
|
||||||
|
|
||||||
@@ -773,6 +776,7 @@ export default function WatchPage() {
|
|||||||
|
|
||||||
const handleResolveComment = useCallback(
|
const handleResolveComment = useCallback(
|
||||||
async (commentId: string, currentlyResolved: boolean) => {
|
async (commentId: string, currentlyResolved: boolean) => {
|
||||||
|
isMutatingRef.current = true;
|
||||||
// Optimistically toggle
|
// Optimistically toggle
|
||||||
setVideo((prev) => {
|
setVideo((prev) => {
|
||||||
if (!prev) return prev;
|
if (!prev) return prev;
|
||||||
@@ -837,6 +841,8 @@ export default function WatchPage() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
toast.error('Failed to update comment');
|
toast.error('Failed to update comment');
|
||||||
|
} finally {
|
||||||
|
isMutatingRef.current = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeVersionId]
|
[activeVersionId]
|
||||||
@@ -887,6 +893,7 @@ export default function WatchPage() {
|
|||||||
setReplyRecordingTime(0);
|
setReplyRecordingTime(0);
|
||||||
|
|
||||||
setIsSubmittingReply(true);
|
setIsSubmittingReply(true);
|
||||||
|
isMutatingRef.current = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/versions/${activeVersion.id}/comments`, {
|
const res = await fetch(`/api/versions/${activeVersion.id}/comments`, {
|
||||||
@@ -968,6 +975,7 @@ export default function WatchPage() {
|
|||||||
toast.error('Failed to add reply');
|
toast.error('Failed to add reply');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmittingReply(false);
|
setIsSubmittingReply(false);
|
||||||
|
isMutatingRef.current = false;
|
||||||
}
|
}
|
||||||
}, [replyText, activeVersion, activeVersionId, comments, currentTime, isGuest, guestName]);
|
}, [replyText, activeVersion, activeVersionId, comments, currentTime, isGuest, guestName]);
|
||||||
|
|
||||||
@@ -1043,6 +1051,7 @@ export default function WatchPage() {
|
|||||||
const handleEditComment = useCallback(async (commentId: string) => {
|
const handleEditComment = useCallback(async (commentId: string) => {
|
||||||
if (!editText.trim()) return;
|
if (!editText.trim()) return;
|
||||||
setIsSubmittingEdit(true);
|
setIsSubmittingEdit(true);
|
||||||
|
isMutatingRef.current = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/comments/${commentId}`, {
|
const res = await fetch(`/api/comments/${commentId}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@@ -1079,12 +1088,14 @@ export default function WatchPage() {
|
|||||||
console.error('Failed to edit comment:', err);
|
console.error('Failed to edit comment:', err);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmittingEdit(false);
|
setIsSubmittingEdit(false);
|
||||||
|
isMutatingRef.current = false;
|
||||||
}
|
}
|
||||||
}, [editText, activeVersionId]);
|
}, [editText, activeVersionId]);
|
||||||
|
|
||||||
// Delete a comment
|
// Delete a comment
|
||||||
const handleDeleteComment = useCallback(async (commentId: string) => {
|
const handleDeleteComment = useCallback(async (commentId: string) => {
|
||||||
setDeletingCommentId(commentId);
|
setDeletingCommentId(commentId);
|
||||||
|
isMutatingRef.current = true;
|
||||||
|
|
||||||
// Optimistically remove from UI
|
// Optimistically remove from UI
|
||||||
const previousVideo = video;
|
const previousVideo = video;
|
||||||
@@ -1118,6 +1129,7 @@ export default function WatchPage() {
|
|||||||
setVideo(previousVideo);
|
setVideo(previousVideo);
|
||||||
} finally {
|
} finally {
|
||||||
setDeletingCommentId(null);
|
setDeletingCommentId(null);
|
||||||
|
isMutatingRef.current = false;
|
||||||
}
|
}
|
||||||
}, [activeVersionId, video]);
|
}, [activeVersionId, video]);
|
||||||
|
|
||||||
@@ -1126,19 +1138,15 @@ export default function WatchPage() {
|
|||||||
if (!video) return;
|
if (!video) return;
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const activeVersion = video.versions.find((v) => v.id === activeVersionId);
|
if (isMutatingRef.current) return;
|
||||||
const hasPendingComments = activeVersion?.comments.some(
|
|
||||||
(c) => c.id.startsWith('temp-')
|
|
||||||
) || activeVersion?.comments.some(
|
|
||||||
(c) => c.replies.some((r) => r.id.startsWith('temp-'))
|
|
||||||
);
|
|
||||||
if (hasPendingComments) return;
|
|
||||||
|
|
||||||
const res = await fetch(`/api/watch/${videoId}`);
|
const res = await fetch(`/api/watch/${videoId}`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
if (!isMutatingRef.current) {
|
||||||
setVideo(data.data);
|
setVideo(data.data);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch { /* silent */ }
|
} catch { /* silent */ }
|
||||||
}, 10000);
|
}, 10000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
|
|||||||
Reference in New Issue
Block a user