From 3b8c4a0cb837e63e54b8ca76f264396b2054a1fc Mon Sep 17 00:00:00 2001 From: Mistral Vibe Date: Mon, 30 Mar 2026 19:06:40 +0200 Subject: [PATCH] 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 --- api/src/rehearsalhub/schemas/comment.py | 2 ++ web/src/hooks/useWaveform.ts | 17 ++++++++++------- web/src/pages/SongPage.tsx | 23 ++++++++++++++++++----- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/api/src/rehearsalhub/schemas/comment.py b/api/src/rehearsalhub/schemas/comment.py index 4872e12..ad7c90a 100644 --- a/api/src/rehearsalhub/schemas/comment.py +++ b/api/src/rehearsalhub/schemas/comment.py @@ -19,6 +19,7 @@ class SongCommentRead(BaseModel): body: str author_id: uuid.UUID author_name: str + author_avatar_url: str | None timestamp: float | None created_at: datetime @@ -30,6 +31,7 @@ class SongCommentRead(BaseModel): body=getattr(c, "body"), author_id=getattr(c, "author_id"), author_name=getattr(getattr(c, "author"), "display_name"), + author_avatar_url=getattr(getattr(c, "author"), "avatar_url"), timestamp=getattr(c, "timestamp"), created_at=getattr(c, "created_at"), ) diff --git a/web/src/hooks/useWaveform.ts b/web/src/hooks/useWaveform.ts index de20cc8..aff3f23 100644 --- a/web/src/hooks/useWaveform.ts +++ b/web/src/hooks/useWaveform.ts @@ -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); } diff --git a/web/src/pages/SongPage.tsx b/web/src/pages/SongPage.tsx index 8141799..0445c27 100644 --- a/web/src/pages/SongPage.tsx +++ b/web/src/pages/SongPage.tsx @@ -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 && (