mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-10 04:22:00 +10:00
feat: websocket rooms (#515)
This commit is contained in:
@ -10,9 +10,7 @@ import {
|
|||||||
pageEditorAtom,
|
pageEditorAtom,
|
||||||
titleEditorAtom,
|
titleEditorAtom,
|
||||||
} from "@/features/editor/atoms/editor-atoms";
|
} from "@/features/editor/atoms/editor-atoms";
|
||||||
import {
|
import { useUpdatePageMutation } from "@/features/page/queries/page-query";
|
||||||
useUpdatePageMutation,
|
|
||||||
} from "@/features/page/queries/page-query";
|
|
||||||
import { useDebouncedValue } from "@mantine/hooks";
|
import { useDebouncedValue } from "@mantine/hooks";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom";
|
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom";
|
||||||
@ -39,7 +37,11 @@ export function TitleEditor({
|
|||||||
}: TitleEditorProps) {
|
}: TitleEditorProps) {
|
||||||
const [debouncedTitleState, setDebouncedTitleState] = useState(null);
|
const [debouncedTitleState, setDebouncedTitleState] = useState(null);
|
||||||
const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 500);
|
const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 500);
|
||||||
const updatePageMutation = useUpdatePageMutation();
|
const {
|
||||||
|
data: updatedPageData,
|
||||||
|
mutate: updatePageMutation,
|
||||||
|
status,
|
||||||
|
} = useUpdatePageMutation();
|
||||||
const pageEditor = useAtomValue(pageEditorAtom);
|
const pageEditor = useAtomValue(pageEditorAtom);
|
||||||
const [, setTitleEditor] = useAtom(titleEditorAtom);
|
const [, setTitleEditor] = useAtom(titleEditorAtom);
|
||||||
const [treeData, setTreeData] = useAtom(treeDataAtom);
|
const [treeData, setTreeData] = useAtom(treeDataAtom);
|
||||||
@ -47,7 +49,6 @@ export function TitleEditor({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [activePageId, setActivePageId] = useState(pageId);
|
const [activePageId, setActivePageId] = useState(pageId);
|
||||||
|
|
||||||
|
|
||||||
const titleEditor = useEditor({
|
const titleEditor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
Document.extend({
|
Document.extend({
|
||||||
@ -87,24 +88,29 @@ export function TitleEditor({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (debouncedTitle !== null && activePageId === pageId) {
|
if (debouncedTitle !== null && activePageId === pageId) {
|
||||||
updatePageMutation.mutate({
|
updatePageMutation({
|
||||||
pageId: pageId,
|
pageId: pageId,
|
||||||
title: debouncedTitle,
|
title: debouncedTitle,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}, [debouncedTitle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === "success" && updatedPageData) {
|
||||||
|
const newTreeData = updateTreeNodeName(treeData, pageId, debouncedTitle);
|
||||||
|
setTreeData(newTreeData);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
emit({
|
emit({
|
||||||
operation: "updateOne",
|
operation: "updateOne",
|
||||||
|
spaceId: updatedPageData.spaceId,
|
||||||
entity: ["pages"],
|
entity: ["pages"],
|
||||||
id: pageId,
|
id: pageId,
|
||||||
payload: { title: debouncedTitle, slugId: slugId },
|
payload: { title: debouncedTitle, slugId: slugId },
|
||||||
});
|
});
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
const newTreeData = updateTreeNodeName(treeData, pageId, debouncedTitle);
|
|
||||||
setTreeData(newTreeData);
|
|
||||||
}
|
}
|
||||||
}, [debouncedTitle]);
|
}, [updatedPageData, status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (titleEditor && title !== titleEditor.getText()) {
|
if (titleEditor && title !== titleEditor.getText()) {
|
||||||
|
|||||||
@ -133,13 +133,13 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
|
|||||||
flatTreeItems = [
|
flatTreeItems = [
|
||||||
...flatTreeItems,
|
...flatTreeItems,
|
||||||
...children.filter(
|
...children.filter(
|
||||||
(child) => !flatTreeItems.some((item) => item.id === child.id),
|
(child) => !flatTreeItems.some((item) => item.id === child.id)
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPromises = ancestors.map((ancestor) =>
|
const fetchPromises = ancestors.map((ancestor) =>
|
||||||
fetchAndUpdateChildren(ancestor),
|
fetchAndUpdateChildren(ancestor)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait for all fetch operations to complete
|
// Wait for all fetch operations to complete
|
||||||
@ -153,7 +153,7 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
|
|||||||
const updatedTree = appendNodeChildren(
|
const updatedTree = appendNodeChildren(
|
||||||
data,
|
data,
|
||||||
rootChild.id,
|
rootChild.id,
|
||||||
rootChild.children,
|
rootChild.children
|
||||||
);
|
);
|
||||||
setData(updatedTree);
|
setData(updatedTree);
|
||||||
|
|
||||||
@ -248,7 +248,7 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps<any>) {
|
|||||||
const updatedTreeData = appendNodeChildren(
|
const updatedTreeData = appendNodeChildren(
|
||||||
treeData,
|
treeData,
|
||||||
node.data.id,
|
node.data.id,
|
||||||
childrenTree,
|
childrenTree
|
||||||
);
|
);
|
||||||
|
|
||||||
setTreeData(updatedTreeData);
|
setTreeData(updatedTreeData);
|
||||||
@ -279,6 +279,7 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps<any>) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
emit({
|
emit({
|
||||||
operation: "updateOne",
|
operation: "updateOne",
|
||||||
|
spaceId: node.data.spaceId,
|
||||||
entity: ["pages"],
|
entity: ["pages"],
|
||||||
id: node.id,
|
id: node.id,
|
||||||
payload: { icon: emoji.native },
|
payload: { icon: emoji.native },
|
||||||
@ -293,6 +294,7 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps<any>) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
emit({
|
emit({
|
||||||
operation: "updateOne",
|
operation: "updateOne",
|
||||||
|
spaceId: node.data.spaceId,
|
||||||
entity: ["pages"],
|
entity: ["pages"],
|
||||||
id: node.id,
|
id: node.id,
|
||||||
payload: { icon: null },
|
payload: { icon: null },
|
||||||
|
|||||||
@ -75,18 +75,19 @@ export function useTreeMutation<T>(spaceId: string) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
emit({
|
emit({
|
||||||
operation: "addTreeNode",
|
operation: "addTreeNode",
|
||||||
|
spaceId: spaceId,
|
||||||
payload: {
|
payload: {
|
||||||
parentId,
|
parentId,
|
||||||
index,
|
index,
|
||||||
data
|
data,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
const pageUrl = buildPageUrl(
|
const pageUrl = buildPageUrl(
|
||||||
spaceSlug,
|
spaceSlug,
|
||||||
createdPage.slugId,
|
createdPage.slugId,
|
||||||
createdPage.title,
|
createdPage.title
|
||||||
);
|
);
|
||||||
navigate(pageUrl);
|
navigate(pageUrl);
|
||||||
return data;
|
return data;
|
||||||
@ -156,18 +157,16 @@ export function useTreeMutation<T>(spaceId: string) {
|
|||||||
// check if the previous still has children
|
// check if the previous still has children
|
||||||
// if no children left, change 'hasChildren' to false, to make the page toggle arrows work properly
|
// if no children left, change 'hasChildren' to false, to make the page toggle arrows work properly
|
||||||
const childrenCount = previousParent.children.filter(
|
const childrenCount = previousParent.children.filter(
|
||||||
(child) => child.id !== draggedNodeId,
|
(child) => child.id !== draggedNodeId
|
||||||
).length;
|
).length;
|
||||||
if (childrenCount === 0) {
|
if (childrenCount === 0) {
|
||||||
tree.update({
|
tree.update({
|
||||||
id: previousParent.id,
|
id: previousParent.id,
|
||||||
changes: { ... previousParent.data, hasChildren: false } as any,
|
changes: { ...previousParent.data, hasChildren: false } as any,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log()
|
|
||||||
|
|
||||||
setData(tree.data);
|
setData(tree.data);
|
||||||
|
|
||||||
const payload: IMovePage = {
|
const payload: IMovePage = {
|
||||||
@ -182,7 +181,13 @@ export function useTreeMutation<T>(spaceId: string) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
emit({
|
emit({
|
||||||
operation: "moveTreeNode",
|
operation: "moveTreeNode",
|
||||||
payload: { id: draggedNodeId, parentId: args.parentId, index: args.index, position: newPosition },
|
spaceId: spaceId,
|
||||||
|
payload: {
|
||||||
|
id: draggedNodeId,
|
||||||
|
parentId: args.parentId,
|
||||||
|
index: args.index,
|
||||||
|
position: newPosition,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}, 50);
|
}, 50);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -214,17 +219,17 @@ export function useTreeMutation<T>(spaceId: string) {
|
|||||||
setData(tree.data);
|
setData(tree.data);
|
||||||
|
|
||||||
// navigate only if the current url is same as the deleted page
|
// navigate only if the current url is same as the deleted page
|
||||||
if (pageSlug && node.data.slugId === pageSlug.split('-')[1]) {
|
if (pageSlug && node.data.slugId === pageSlug.split("-")[1]) {
|
||||||
navigate(getSpaceUrl(spaceSlug));
|
navigate(getSpaceUrl(spaceSlug));
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
emit({
|
emit({
|
||||||
operation: "deleteTreeNode",
|
operation: "deleteTreeNode",
|
||||||
payload: { node: node.data }
|
spaceId: spaceId,
|
||||||
|
payload: { node: node.data },
|
||||||
});
|
});
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete page:", error);
|
console.error("Failed to delete page:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,14 @@ import { SpaceTreeNode } from "@/features/page/tree/types.ts";
|
|||||||
|
|
||||||
export type InvalidateEvent = {
|
export type InvalidateEvent = {
|
||||||
operation: "invalidate";
|
operation: "invalidate";
|
||||||
|
spaceId: string;
|
||||||
entity: Array<string>;
|
entity: Array<string>;
|
||||||
id?: string;
|
id?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateEvent = {
|
export type UpdateEvent = {
|
||||||
operation: "updateOne";
|
operation: "updateOne";
|
||||||
|
spaceId: string;
|
||||||
entity: Array<string>;
|
entity: Array<string>;
|
||||||
id: string;
|
id: string;
|
||||||
payload: Partial<any>;
|
payload: Partial<any>;
|
||||||
@ -15,6 +17,7 @@ export type UpdateEvent = {
|
|||||||
|
|
||||||
export type DeleteEvent = {
|
export type DeleteEvent = {
|
||||||
operation: "deleteOne";
|
operation: "deleteOne";
|
||||||
|
spaceId: string;
|
||||||
entity: Array<string>;
|
entity: Array<string>;
|
||||||
id: string;
|
id: string;
|
||||||
payload?: Partial<any>;
|
payload?: Partial<any>;
|
||||||
@ -22,6 +25,7 @@ export type DeleteEvent = {
|
|||||||
|
|
||||||
export type AddTreeNodeEvent = {
|
export type AddTreeNodeEvent = {
|
||||||
operation: "addTreeNode";
|
operation: "addTreeNode";
|
||||||
|
spaceId: string;
|
||||||
payload: {
|
payload: {
|
||||||
parentId: string;
|
parentId: string;
|
||||||
index: number;
|
index: number;
|
||||||
@ -31,6 +35,7 @@ export type AddTreeNodeEvent = {
|
|||||||
|
|
||||||
export type MoveTreeNodeEvent = {
|
export type MoveTreeNodeEvent = {
|
||||||
operation: "moveTreeNode";
|
operation: "moveTreeNode";
|
||||||
|
spaceId: string;
|
||||||
payload: {
|
payload: {
|
||||||
id: string;
|
id: string;
|
||||||
parentId: string;
|
parentId: string;
|
||||||
@ -41,6 +46,7 @@ export type MoveTreeNodeEvent = {
|
|||||||
|
|
||||||
export type DeleteTreeNodeEvent = {
|
export type DeleteTreeNodeEvent = {
|
||||||
operation: "deleteTreeNode";
|
operation: "deleteTreeNode";
|
||||||
|
spaceId: string;
|
||||||
payload: {
|
payload: {
|
||||||
node: SpaceTreeNode
|
node: SpaceTreeNode
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { Server, Socket } from 'socket.io';
|
|||||||
import { TokenService } from '../core/auth/services/token.service';
|
import { TokenService } from '../core/auth/services/token.service';
|
||||||
import { JwtType } from '../core/auth/dto/jwt-payload';
|
import { JwtType } from '../core/auth/dto/jwt-payload';
|
||||||
import { OnModuleDestroy } from '@nestjs/common';
|
import { OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||||
|
|
||||||
@WebSocketGateway({
|
@WebSocketGateway({
|
||||||
cors: { origin: '*' },
|
cors: { origin: '*' },
|
||||||
@ -17,7 +18,10 @@ import { OnModuleDestroy } from '@nestjs/common';
|
|||||||
export class WsGateway implements OnGatewayConnection, OnModuleDestroy {
|
export class WsGateway implements OnGatewayConnection, OnModuleDestroy {
|
||||||
@WebSocketServer()
|
@WebSocketServer()
|
||||||
server: Server;
|
server: Server;
|
||||||
constructor(private tokenService: TokenService) {}
|
constructor(
|
||||||
|
private tokenService: TokenService,
|
||||||
|
private spaceMemberRepo: SpaceMemberRepo,
|
||||||
|
) {}
|
||||||
|
|
||||||
async handleConnection(client: Socket, ...args: any[]): Promise<void> {
|
async handleConnection(client: Socket, ...args: any[]): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@ -27,24 +31,43 @@ export class WsGateway implements OnGatewayConnection, OnModuleDestroy {
|
|||||||
if (token.type !== JwtType.ACCESS) {
|
if (token.type !== JwtType.ACCESS) {
|
||||||
client.disconnect();
|
client.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userId = token.sub;
|
||||||
|
const workspaceId = token.workspaceId;
|
||||||
|
|
||||||
|
const userSpaceIds = await this.spaceMemberRepo.getUserSpaceIds(userId);
|
||||||
|
|
||||||
|
const workspaceRoom = `workspace-${workspaceId}`;
|
||||||
|
const spaceRooms = userSpaceIds.map((id) => this.getSpaceRoomName(id));
|
||||||
|
|
||||||
|
client.join([workspaceRoom, ...spaceRooms]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
client.disconnect();
|
client.disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SubscribeMessage('message')
|
@SubscribeMessage('message')
|
||||||
handleMessage(client: Socket, data: string): void {
|
handleMessage(client: Socket, data: any): void {
|
||||||
client.broadcast.emit('message', data);
|
const spaceEvents = [
|
||||||
}
|
'updateOne',
|
||||||
|
'addTreeNode',
|
||||||
|
'moveTreeNode',
|
||||||
|
'deleteTreeNode',
|
||||||
|
];
|
||||||
|
|
||||||
@SubscribeMessage('messageToRoom')
|
if (spaceEvents.includes(data?.operation) && data?.spaceId) {
|
||||||
handleSendMessageToRoom(@MessageBody() message: any) {
|
const room = this.getSpaceRoomName(data.spaceId);
|
||||||
this.server.to(message?.roomId).emit('messageToRoom', message);
|
client.broadcast.to(room).emit('message', data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.broadcast.emit('message', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SubscribeMessage('join-room')
|
@SubscribeMessage('join-room')
|
||||||
handleJoinRoom(client: Socket, @MessageBody() roomName: string): void {
|
handleJoinRoom(client: Socket, @MessageBody() roomName: string): void {
|
||||||
client.join(roomName);
|
// if room is a space, check if user has permissions
|
||||||
|
//client.join(roomName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SubscribeMessage('leave-room')
|
@SubscribeMessage('leave-room')
|
||||||
@ -57,4 +80,8 @@ export class WsGateway implements OnGatewayConnection, OnModuleDestroy {
|
|||||||
this.server.close();
|
this.server.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSpaceRoomName(spaceId: string): string {
|
||||||
|
return `space-${spaceId}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user