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:
Mistral Vibe
2026-03-30 19:06:40 +02:00
parent 86d4c8fad6
commit 3b8c4a0cb8
3 changed files with 30 additions and 12 deletions

View File

@@ -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);
}

View File

@@ -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" }}
>