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:
@@ -19,6 +19,7 @@ class SongCommentRead(BaseModel):
|
|||||||
body: str
|
body: str
|
||||||
author_id: uuid.UUID
|
author_id: uuid.UUID
|
||||||
author_name: str
|
author_name: str
|
||||||
|
author_avatar_url: str | None
|
||||||
timestamp: float | None
|
timestamp: float | None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ class SongCommentRead(BaseModel):
|
|||||||
body=getattr(c, "body"),
|
body=getattr(c, "body"),
|
||||||
author_id=getattr(c, "author_id"),
|
author_id=getattr(c, "author_id"),
|
||||||
author_name=getattr(getattr(c, "author"), "display_name"),
|
author_name=getattr(getattr(c, "author"), "display_name"),
|
||||||
|
author_avatar_url=getattr(getattr(c, "author"), "avatar_url"),
|
||||||
timestamp=getattr(c, "timestamp"),
|
timestamp=getattr(c, "timestamp"),
|
||||||
created_at=getattr(c, "created_at"),
|
created_at=getattr(c, "created_at"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ export function useWaveform(
|
|||||||
const wavesurfer = wsRef.current;
|
const wavesurfer = wsRef.current;
|
||||||
const markerElement = document.createElement("div");
|
const markerElement = document.createElement("div");
|
||||||
markerElement.style.position = "absolute";
|
markerElement.style.position = "absolute";
|
||||||
markerElement.style.width = "20px";
|
markerElement.style.width = "24px";
|
||||||
markerElement.style.height = "20px";
|
markerElement.style.height = "24px";
|
||||||
markerElement.style.borderRadius = "50%";
|
markerElement.style.borderRadius = "50%";
|
||||||
markerElement.style.backgroundColor = "var(--accent)";
|
markerElement.style.backgroundColor = "var(--accent)";
|
||||||
markerElement.style.cursor = "pointer";
|
markerElement.style.cursor = "pointer";
|
||||||
@@ -106,6 +106,8 @@ export function useWaveform(
|
|||||||
markerElement.style.left = `${(marker.time / wavesurfer.getDuration()) * 100}%`;
|
markerElement.style.left = `${(marker.time / wavesurfer.getDuration()) * 100}%`;
|
||||||
markerElement.style.transform = "translateX(-50%) translateY(-50%)";
|
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.title = `Comment at ${formatTime(marker.time)}`;
|
||||||
markerElement.onclick = marker.onClick;
|
markerElement.onclick = marker.onClick;
|
||||||
|
|
||||||
@@ -115,6 +117,7 @@ export function useWaveform(
|
|||||||
iconElement.style.width = "100%";
|
iconElement.style.width = "100%";
|
||||||
iconElement.style.height = "100%";
|
iconElement.style.height = "100%";
|
||||||
iconElement.style.borderRadius = "50%";
|
iconElement.style.borderRadius = "50%";
|
||||||
|
iconElement.style.objectFit = "cover";
|
||||||
markerElement.appendChild(iconElement);
|
markerElement.appendChild(iconElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ interface SongComment {
|
|||||||
body: string;
|
body: string;
|
||||||
author_id: string;
|
author_id: string;
|
||||||
author_name: string;
|
author_name: string;
|
||||||
|
author_avatar_url: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
timestamp: number; // Timestamp in seconds
|
timestamp: number | null; // Timestamp in seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SongPage() {
|
export function SongPage() {
|
||||||
@@ -80,26 +81,38 @@ export function SongPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (comments) {
|
if (comments) {
|
||||||
|
console.log('Comments data:', comments);
|
||||||
clearMarkers();
|
clearMarkers();
|
||||||
comments.forEach((comment) => {
|
comments.forEach((comment) => {
|
||||||
|
console.log('Processing comment:', comment.id, 'timestamp:', comment.timestamp, 'avatar:', comment.author_avatar_url);
|
||||||
if (comment.timestamp !== undefined && comment.timestamp !== null) {
|
if (comment.timestamp !== undefined && comment.timestamp !== null) {
|
||||||
|
console.log('Adding marker at time:', comment.timestamp);
|
||||||
addMarker({
|
addMarker({
|
||||||
id: comment.id,
|
id: comment.id,
|
||||||
time: comment.timestamp,
|
time: comment.timestamp,
|
||||||
onClick: () => scrollToComment(comment.id),
|
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]);
|
}, [comments, addMarker, clearMarkers]);
|
||||||
|
|
||||||
const addCommentMutation = useMutation({
|
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: () => {
|
onSuccess: () => {
|
||||||
|
console.log('Comment created successfully');
|
||||||
qc.invalidateQueries({ queryKey: ["comments", songId] });
|
qc.invalidateQueries({ queryKey: ["comments", songId] });
|
||||||
setCommentBody("");
|
setCommentBody("");
|
||||||
},
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Error creating comment:', error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteCommentMutation = useMutation({
|
const deleteCommentMutation = useMutation({
|
||||||
@@ -196,7 +209,7 @@ export function SongPage() {
|
|||||||
{c.timestamp !== undefined && c.timestamp !== null && (
|
{c.timestamp !== undefined && c.timestamp !== null && (
|
||||||
<div style={{ display: "flex", gap: 8, marginBottom: 6 }}>
|
<div style={{ display: "flex", gap: 8, marginBottom: 6 }}>
|
||||||
<button
|
<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" }}
|
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)}
|
{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" }}
|
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
|
<button
|
||||||
onClick={() => commentBody.trim() && addCommentMutation.mutate(commentBody.trim())}
|
onClick={() => commentBody.trim() && addCommentMutation.mutate({ body: commentBody.trim(), timestamp: currentTime })}
|
||||||
disabled={!commentBody.trim() || addCommentMutation.isPending}
|
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" }}
|
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