Files
docmost/apps/client/src/features/websocket/use-query-subscription.ts
T
Philip Okugbe ca9558b246 feat(EE): resolve comments (#1420)
* feat: resolve comment (EE)

* Add resolve to comment mark in editor (EE)

* comment ui permissions

* sticky comment state tabs (EE)

* cleanup

* feat: add space_id to comments and allow space admins to delete any comment

- Add space_id column to comments table with data migration from pages
- Add last_edited_by_id, resolved_by_id, and updated_at columns to comments
- Update comment deletion permissions to allow space admins to delete any comment
- Backfill space_id on old comments

* fix foreign keys
2025-07-29 21:36:48 +01:00

128 lines
4.0 KiB
TypeScript

import React from "react";
import { socketAtom } from "@/features/websocket/atoms/socket-atom.ts";
import { useAtom } from "jotai";
import { InfiniteData, useQueryClient } from "@tanstack/react-query";
import { WebSocketEvent } from "@/features/websocket/types";
import { IPage } from "../page/types/page.types";
import { IPagination } from "@/lib/types";
import {
invalidateOnCreatePage,
invalidateOnDeletePage,
invalidateOnMovePage,
invalidateOnUpdatePage,
} from "../page/queries/page-query";
import { RQ_KEY } from "../comment/queries/comment-query";
import { queryClient } from "@/main.tsx";
import { IComment } from "@/features/comment/types/comment.types";
export const useQuerySubscription = () => {
const queryClient = useQueryClient();
const [socket] = useAtom(socketAtom);
React.useEffect(() => {
socket?.on("message", (event) => {
const data: WebSocketEvent = event;
let entity = null;
let queryKeyId = null;
switch (data.operation) {
case "invalidate":
queryClient.invalidateQueries({
queryKey: [...data.entity, data.id].filter(Boolean),
});
break;
case "invalidateComment":
queryClient.invalidateQueries({
queryKey: RQ_KEY(data.pageId),
});
break;
case "addTreeNode":
invalidateOnCreatePage(data.payload.data);
break;
case "moveTreeNode":
invalidateOnMovePage();
break;
case "deleteTreeNode":
invalidateOnDeletePage(data.payload.node.id);
break;
case "updateOne":
entity = data.entity[0];
if (entity === "pages") {
// we have to do this because the usePageQuery cache key is the slugId.
queryKeyId = data.payload.slugId;
} else {
queryKeyId = data.id;
}
// only update if data was already in cache
if (queryClient.getQueryData([...data.entity, queryKeyId])) {
queryClient.setQueryData([...data.entity, queryKeyId], {
...queryClient.getQueryData([...data.entity, queryKeyId]),
...data.payload,
});
}
if (entity === "pages") {
invalidateOnUpdatePage(
data.spaceId,
data.payload.parentPageId,
data.id,
data.payload.title,
data.payload.icon,
);
}
/*
queryClient.setQueriesData(
{ queryKey: [data.entity, data.id] },
(oldData: any) => {
const update = (entity: Record<string, unknown>) =>
entity.id === data.id ? { ...entity, ...data.payload } : entity;
return Array.isArray(oldData)
? oldData.map(update)
: update(oldData as Record<string, unknown>);
},
);
*/
break;
case "refetchRootTreeNodeEvent": {
const spaceId = data.spaceId;
queryClient.refetchQueries({
queryKey: ["root-sidebar-pages", spaceId],
});
queryClient.invalidateQueries({
queryKey: ["recent-changes", spaceId],
});
break;
}
case "resolveComment": {
const currentComments = queryClient.getQueryData(
RQ_KEY(data.pageId),
) as IPagination<IComment>;
if (currentComments && currentComments.items) {
const updatedComments = currentComments.items.map((comment) =>
comment.id === data.commentId
? {
...comment,
resolvedAt: data.resolvedAt,
resolvedById: data.resolvedById,
resolvedBy: data.resolvedBy
}
: comment,
);
queryClient.setQueryData(RQ_KEY(data.pageId), {
...currentComments,
items: updatedComments,
});
}
break;
}
}
});
}, [queryClient, socket]);
};