diff --git a/apps/client/src/features/comment/components/comment-dialog.tsx b/apps/client/src/features/comment/components/comment-dialog.tsx index 2e27f65b..080c731f 100644 --- a/apps/client/src/features/comment/components/comment-dialog.tsx +++ b/apps/client/src/features/comment/components/comment-dialog.tsx @@ -15,6 +15,7 @@ import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar- import { useEditor } from "@tiptap/react"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; import { useTranslation } from "react-i18next"; +import { useQueryEmit } from "@/features/websocket/use-query-emit"; interface CommentDialogProps { editor: ReturnType; @@ -35,6 +36,8 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) { const createCommentMutation = useCreateCommentMutation(); const { isPending } = createCommentMutation; + const emit = useQueryEmit(); + const handleDialogClose = () => { setShowCommentPopup(false); editor.chain().focus().unsetCommentDecoration().run(); @@ -63,11 +66,23 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) { .run(); setActiveCommentId(createdComment.id); + //unselect text to close bubble menu + editor.commands.setTextSelection({ from: editor.view.state.selection.from, to: editor.view.state.selection.from }); + setAsideState({ tab: "comments", isAsideOpen: true }); setTimeout(() => { const selector = `div[data-comment-id="${createdComment.id}"]`; const commentElement = document.querySelector(selector); - commentElement?.scrollIntoView(); + commentElement?.scrollIntoView({ behavior: "smooth", block: "center" }); + + editor.view.dispatch( + editor.state.tr.scrollIntoView() + ); + }, 400); + + emit({ + operation: "invalidateComment", + pageId: pageId, }); } finally { setShowCommentPopup(false); diff --git a/apps/client/src/features/comment/components/comment-editor.tsx b/apps/client/src/features/comment/components/comment-editor.tsx index 09116f1a..32339a3d 100644 --- a/apps/client/src/features/comment/components/comment-editor.tsx +++ b/apps/client/src/features/comment/components/comment-editor.tsx @@ -53,6 +53,10 @@ const CommentEditor = forwardRef( autofocus: (autofocus && "end") || false, }); + useEffect(() => { + commentEditor.commands.setContent(defaultContent); + }, [defaultContent]); + useEffect(() => { setTimeout(() => { if (autofocus) { diff --git a/apps/client/src/features/comment/components/comment-list-item.tsx b/apps/client/src/features/comment/components/comment-list-item.tsx index 293c4f9e..9e136a1d 100644 --- a/apps/client/src/features/comment/components/comment-list-item.tsx +++ b/apps/client/src/features/comment/components/comment-list-item.tsx @@ -1,5 +1,5 @@ import { Group, Text, Box } from "@mantine/core"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import classes from "./comment.module.css"; import { useAtom, useAtomValue } from "jotai"; import { timeAgo } from "@/lib/time"; @@ -15,12 +15,14 @@ import { import { IComment } from "@/features/comment/types/comment.types"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts"; +import { useQueryEmit } from "@/features/websocket/use-query-emit"; interface CommentListItemProps { comment: IComment; + pageId: string; } -function CommentListItem({ comment }: CommentListItemProps) { +function CommentListItem({ comment, pageId }: CommentListItemProps) { const { hovered, ref } = useHover(); const [isEditing, setIsEditing] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -29,6 +31,11 @@ function CommentListItem({ comment }: CommentListItemProps) { const updateCommentMutation = useUpdateCommentMutation(); const deleteCommentMutation = useDeleteCommentMutation(comment.pageId); const [currentUser] = useAtom(currentUserAtom); + const emit = useQueryEmit(); + + useEffect(() => { + setContent(comment.content) + }, [comment]); async function handleUpdateComment() { try { @@ -39,6 +46,11 @@ function CommentListItem({ comment }: CommentListItemProps) { }; await updateCommentMutation.mutateAsync(commentToUpdate); setIsEditing(false); + + emit({ + operation: "invalidateComment", + pageId: pageId, + }); } catch (error) { console.error("Failed to update comment:", error); } finally { @@ -50,11 +62,27 @@ function CommentListItem({ comment }: CommentListItemProps) { try { await deleteCommentMutation.mutateAsync(comment.id); editor?.commands.unsetComment(comment.id); + + emit({ + operation: "invalidateComment", + pageId: pageId, + }); } catch (error) { console.error("Failed to delete comment:", error); } } + function handleCommentClick(comment: IComment) { + const el = document.querySelector(`.comment-mark[data-comment-id="${comment.id}"]`); + if (el) { + el.scrollIntoView({ behavior: "smooth", block: "center" }); + el.classList.add("comment-highlight"); + setTimeout(() => { + el.classList.remove("comment-highlight"); + }, 3000); + } + } + function handleEditToggle() { setIsEditing(true); } @@ -99,7 +127,7 @@ function CommentListItem({ comment }: CommentListItemProps) {
{!comment.parentCommentId && comment?.selection && ( - + handleCommentClick(comment)}> {comment?.selection} )} diff --git a/apps/client/src/features/comment/components/comment-list.tsx b/apps/client/src/features/comment/components/comment-list.tsx index 3296d5ea..6f0d877a 100644 --- a/apps/client/src/features/comment/components/comment-list.tsx +++ b/apps/client/src/features/comment/components/comment-list.tsx @@ -14,6 +14,7 @@ import { usePageQuery } from "@/features/page/queries/page-query.ts"; import { IPagination } from "@/lib/types.ts"; import { extractPageSlugId } from "@/lib"; import { useTranslation } from "react-i18next"; +import { useQueryEmit } from "@/features/websocket/use-query-emit"; function CommentList() { const { t } = useTranslation(); @@ -26,6 +27,7 @@ function CommentList() { } = useCommentsQuery({ pageId: page?.id, limit: 100 }); const createCommentMutation = useCreateCommentMutation(); const [isLoading, setIsLoading] = useState(false); + const emit = useQueryEmit(); const handleAddReply = useCallback( async (commentId: string, content: string) => { @@ -38,6 +40,11 @@ function CommentList() { }; await createCommentMutation.mutateAsync(commentData); + + emit({ + operation: "invalidateComment", + pageId: page?.id, + }); } catch (error) { console.error("Failed to post comment:", error); } finally { @@ -59,8 +66,8 @@ function CommentList() { data-comment-id={comment.id} >
- - + +
@@ -99,8 +106,9 @@ function CommentList() { interface ChildCommentsProps { comments: IPagination; parentId: string; + pageId: string; } -const ChildComments = ({ comments, parentId }: ChildCommentsProps) => { +const ChildComments = ({ comments, parentId, pageId }: ChildCommentsProps) => { const getChildComments = useCallback( (parentId: string) => comments.items.filter( @@ -113,10 +121,11 @@ const ChildComments = ({ comments, parentId }: ChildCommentsProps) => {
{getChildComments(parentId).map((childComment) => (
- +
))} diff --git a/apps/client/src/features/comment/components/comment.module.css b/apps/client/src/features/comment/components/comment.module.css index b3d0a261..5679c9b2 100644 --- a/apps/client/src/features/comment/components/comment.module.css +++ b/apps/client/src/features/comment/components/comment.module.css @@ -11,6 +11,7 @@ border-left: 2px solid var(--mantine-color-gray-6); padding: 8px; background: var(--mantine-color-gray-light); + cursor: pointer; } .commentEditor { diff --git a/apps/client/src/features/editor/page-editor.tsx b/apps/client/src/features/editor/page-editor.tsx index 32845c6d..07d9da74 100644 --- a/apps/client/src/features/editor/page-editor.tsx +++ b/apps/client/src/features/editor/page-editor.tsx @@ -219,9 +219,12 @@ export default function PageEditor({ setActiveCommentId(commentId); setAsideState({ tab: "comments", isAsideOpen: true }); - const selector = `div[data-comment-id="${commentId}"]`; - const commentElement = document.querySelector(selector); - commentElement?.scrollIntoView(); + //wait if aside is closed + setTimeout(() => { + const selector = `div[data-comment-id="${commentId}"]`; + const commentElement = document.querySelector(selector); + commentElement?.scrollIntoView({ behavior: "smooth", block: "center" }); + }, 400); }; useEffect(() => { diff --git a/apps/client/src/features/editor/styles/core.css b/apps/client/src/features/editor/styles/core.css index 952e91fb..38acb74b 100644 --- a/apps/client/src/features/editor/styles/core.css +++ b/apps/client/src/features/editor/styles/core.css @@ -144,6 +144,19 @@ border-bottom: 2px solid rgb(166, 158, 12); } + .comment-highlight { + animation: flash-highlight 3s ease-out; + } + + @keyframes flash-highlight { + 0% { + background-color: #ff4d4d; + } + 100% { + background-color: rgba(255, 215, 0, 0.14); + } + } + .resize-cursor { cursor: ew-resize; cursor: col-resize; diff --git a/apps/client/src/features/websocket/types/types.ts b/apps/client/src/features/websocket/types/types.ts index a3e32ac2..48e7d819 100644 --- a/apps/client/src/features/websocket/types/types.ts +++ b/apps/client/src/features/websocket/types/types.ts @@ -7,6 +7,11 @@ export type InvalidateEvent = { id?: string; }; +export type InvalidateCommentsEvent = { + operation: "invalidateComment"; + pageId: string; +}; + export type UpdateEvent = { operation: "updateOne"; spaceId: string; @@ -52,4 +57,4 @@ export type DeleteTreeNodeEvent = { } }; -export type WebSocketEvent = InvalidateEvent | UpdateEvent | DeleteEvent | AddTreeNodeEvent | MoveTreeNodeEvent | DeleteTreeNodeEvent; +export type WebSocketEvent = InvalidateEvent | InvalidateCommentsEvent | UpdateEvent | DeleteEvent | AddTreeNodeEvent | MoveTreeNodeEvent | DeleteTreeNodeEvent; diff --git a/apps/client/src/features/websocket/use-query-subscription.ts b/apps/client/src/features/websocket/use-query-subscription.ts index 576b6777..c9e53aa6 100644 --- a/apps/client/src/features/websocket/use-query-subscription.ts +++ b/apps/client/src/features/websocket/use-query-subscription.ts @@ -3,6 +3,7 @@ import { socketAtom } from "@/features/websocket/atoms/socket-atom.ts"; import { useAtom } from "jotai"; import { useQueryClient } from "@tanstack/react-query"; import { WebSocketEvent } from "@/features/websocket/types"; +import { RQ_KEY } from "../comment/queries/comment-query"; export const useQuerySubscription = () => { const queryClient = useQueryClient(); @@ -21,6 +22,11 @@ export const useQuerySubscription = () => { queryKey: [...data.entity, data.id].filter(Boolean), }); break; + case "invalidateComment": + queryClient.invalidateQueries({ + queryKey: RQ_KEY(data.pageId), + }); + break; case "updateOne": entity = data.entity[0]; if (entity === "pages") {