feat: websocket rooms (#515)

This commit is contained in:
Philip Okugbe
2024-11-28 18:53:29 +00:00
committed by GitHub
parent d97baf5824
commit a16d5d1bf4
5 changed files with 79 additions and 33 deletions

View File

@ -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()) {

View File

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

View File

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

View File

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

View File

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