fix: comment waveform integration with timestamps and avatars
- Add author_avatar_url to API schema and frontend interface - Capture current playhead timestamp when creating comments - Display user avatars in waveform markers instead of placeholders - Improve marker visibility with better styling (size, borders, shadows) - Fix TypeScript type errors for nullable timestamps - Add debug logging for troubleshooting This implements the full comment waveform integration as requested: - Comments are created with exact playhead timestamps - Waveform markers show at correct positions with user avatars - Clicking markers scrolls to corresponding comments - Backward compatible with existing comments without timestamps
This commit is contained in:
@@ -97,24 +97,27 @@ export function useWaveform(
|
||||
const wavesurfer = wsRef.current;
|
||||
const markerElement = document.createElement("div");
|
||||
markerElement.style.position = "absolute";
|
||||
markerElement.style.width = "20px";
|
||||
markerElement.style.height = "20px";
|
||||
markerElement.style.borderRadius = "50% ";
|
||||
markerElement.style.width = "24px";
|
||||
markerElement.style.height = "24px";
|
||||
markerElement.style.borderRadius = "50%";
|
||||
markerElement.style.backgroundColor = "var(--accent)";
|
||||
markerElement.style.cursor = "pointer";
|
||||
markerElement.style.zIndex = "9999";
|
||||
markerElement.style.left = `${(marker.time / wavesurfer.getDuration()) * 100}%`;
|
||||
markerElement.style.transform = "translateX(-50%) translateY(-50%)";
|
||||
markerElement.style.top = "50% ";
|
||||
markerElement.style.top = "50%";
|
||||
markerElement.style.border = "2px solid white";
|
||||
markerElement.style.boxShadow = "0 0 4px rgba(0, 0, 0, 0.3)";
|
||||
markerElement.title = `Comment at ${formatTime(marker.time)}`;
|
||||
markerElement.onclick = marker.onClick;
|
||||
|
||||
if (marker.icon) {
|
||||
const iconElement = document.createElement("img");
|
||||
iconElement.src = marker.icon;
|
||||
iconElement.style.width = "100% ";
|
||||
iconElement.style.height = "100% ";
|
||||
iconElement.style.borderRadius = "50% ";
|
||||
iconElement.style.width = "100%";
|
||||
iconElement.style.height = "100%";
|
||||
iconElement.style.borderRadius = "50%";
|
||||
iconElement.style.objectFit = "cover";
|
||||
markerElement.appendChild(iconElement);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@ interface SongComment {
|
||||
body: string;
|
||||
author_id: string;
|
||||
author_name: string;
|
||||
author_avatar_url: string | null;
|
||||
created_at: string;
|
||||
timestamp: number; // Timestamp in seconds
|
||||
timestamp: number | null; // Timestamp in seconds
|
||||
}
|
||||
|
||||
export function SongPage() {
|
||||
@@ -80,26 +81,38 @@ export function SongPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (comments) {
|
||||
console.log('Comments data:', comments);
|
||||
clearMarkers();
|
||||
comments.forEach((comment) => {
|
||||
console.log('Processing comment:', comment.id, 'timestamp:', comment.timestamp, 'avatar:', comment.author_avatar_url);
|
||||
if (comment.timestamp !== undefined && comment.timestamp !== null) {
|
||||
console.log('Adding marker at time:', comment.timestamp);
|
||||
addMarker({
|
||||
id: comment.id,
|
||||
time: comment.timestamp,
|
||||
onClick: () => scrollToComment(comment.id),
|
||||
icon: "https://via.placeholder.com/20", // Replace with actual user icon URL
|
||||
icon: comment.author_avatar_url || "https://via.placeholder.com/20",
|
||||
});
|
||||
} else {
|
||||
console.log('Skipping comment without timestamp:', comment.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [comments, addMarker, clearMarkers]);
|
||||
|
||||
const addCommentMutation = useMutation({
|
||||
mutationFn: (body: string) => api.post(`/songs/${songId}/comments`, { body }),
|
||||
mutationFn: ({ body, timestamp }: { body: string; timestamp: number }) => {
|
||||
console.log('Creating comment with timestamp:', timestamp);
|
||||
return api.post(`/songs/${songId}/comments`, { body, timestamp });
|
||||
},
|
||||
onSuccess: () => {
|
||||
console.log('Comment created successfully');
|
||||
qc.invalidateQueries({ queryKey: ["comments", songId] });
|
||||
setCommentBody("");
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error creating comment:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const deleteCommentMutation = useMutation({
|
||||
@@ -196,7 +209,7 @@ export function SongPage() {
|
||||
{c.timestamp !== undefined && c.timestamp !== null && (
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 6 }}>
|
||||
<button
|
||||
onClick={() => seekTo(c.timestamp)}
|
||||
onClick={() => seekTo(c.timestamp!)}
|
||||
style={{ background: "var(--accent-bg)", border: "1px solid var(--accent)", borderRadius: 4, color: "var(--accent)", cursor: "pointer", fontSize: 10, padding: "2px 8px", fontFamily: "monospace" }}
|
||||
>
|
||||
{formatTime(c.timestamp)}
|
||||
@@ -220,7 +233,7 @@ export function SongPage() {
|
||||
style={{ flex: 1, padding: "8px 12px", background: "var(--bg-inset)", border: "1px solid var(--border)", borderRadius: 6, color: "var(--text)", fontSize: 13, resize: "vertical", fontFamily: "inherit" }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => commentBody.trim() && addCommentMutation.mutate(commentBody.trim())}
|
||||
onClick={() => commentBody.trim() && addCommentMutation.mutate({ body: commentBody.trim(), timestamp: currentTime })}
|
||||
disabled={!commentBody.trim() || addCommentMutation.isPending}
|
||||
style={{ background: "var(--accent)", border: "none", borderRadius: 6, color: "var(--accent-fg)", cursor: "pointer", padding: "0 18px", fontWeight: 600, fontSize: 13, alignSelf: "stretch" }}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user