From 448a6158480573874b004e07f239fb761e7bcdb9 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 17 Sep 2024 20:13:24 +0530 Subject: [PATCH 01/31] fix: add lock unlock archive restore realtime sync --- live/src/core/hocuspocus-server.ts | 14 ++++ .../core/hooks/use-collaborative-editor.ts | 5 ++ packages/editor/src/core/hooks/use-editor.ts | 5 ++ .../use-read-only-collaborative-editor.ts | 7 +- .../src/core/hooks/use-read-only-editor.ts | 11 +++ packages/editor/src/core/types/editor.ts | 2 + .../pages/editor/header/options-dropdown.tsx | 67 +++++++++++++++++-- 7 files changed, 104 insertions(+), 7 deletions(-) diff --git a/live/src/core/hocuspocus-server.ts b/live/src/core/hocuspocus-server.ts index 7abfebf7f50..4fc8b39f083 100644 --- a/live/src/core/hocuspocus-server.ts +++ b/live/src/core/hocuspocus-server.ts @@ -33,6 +33,20 @@ export const getHocusPocusServer = async () => { throw Error("Authentication unsuccessful!"); } }, + async onStateless({ payload, document }) { + if (payload === "lock") { + document.broadcastStateless("locked"); + } + if (payload === "unlock") { + document.broadcastStateless("unlocked"); + } + if (payload === "archive") { + document.broadcastStateless("archived"); + } + if (payload === "unarchive") { + document.broadcastStateless("unarchived"); + } + }, extensions, }); }; diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 35456068301..3947f1ee297 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -43,6 +43,10 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { onClose: (data) => { if (data.event.code === 1006) serverHandler?.onServerError?.(); }, + onSynced: () => { + console.log("ran", id); + provider.sendStateless("Hello from client"); + }, }), [id, realtimeConfig, serverHandler, user.id] ); @@ -90,6 +94,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { ], placeholder, tabIndex, + provider, }); return { editor }; diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 0542418e5d5..05ea88bb690 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -15,6 +15,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; import { CoreEditorProps } from "@/props"; // types import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TEditorCommands, TFileHandler } from "@/types"; +import { HocuspocusProvider } from "@hocuspocus/provider"; export interface CustomEditorProps { editorClassName: string; @@ -36,6 +37,7 @@ export interface CustomEditorProps { // undefined when prop is not passed, null if intentionally passed to stop // swr syncing value?: string | null | undefined; + provider?: HocuspocusProvider; } export const useEditor = (props: CustomEditorProps) => { @@ -54,6 +56,7 @@ export const useEditor = (props: CustomEditorProps) => { placeholder, tabIndex, value, + provider, } = props; // states const [savedSelection, setSavedSelection] = useState(null); @@ -243,6 +246,8 @@ export const useEditor = (props: CustomEditorProps) => { words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0, }; }, + emitRealTimeUpdate: (message: string) => provider?.sendStateless(message), + listenToRealTimeUpdate: () => provider, }), [editorRef, savedSelection] ); diff --git a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts index fb85fb8a12c..63c7a68ab7d 100644 --- a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts @@ -32,9 +32,13 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit onClose: (data) => { if (data.event.code === 1006) serverHandler?.onServerError?.(); }, + onSynced: () => { + console.log("ran", id); + }, }), - [id, realtimeConfig, user.id] + [id, realtimeConfig, serverHandler, user.id] ); + // destroy and disconnect connection on unmount useEffect( () => () => { @@ -63,6 +67,7 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit document: provider.document, }), ], + provider, }); return { editor, isIndexedDbSynced: true }; diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index 3ee7f8e9075..c93eae5f884 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -10,6 +10,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; import { CoreReadOnlyEditorProps } from "@/props"; // types import { EditorReadOnlyRefApi, IMentionHighlight } from "@/types"; +import { HocuspocusProvider } from "@hocuspocus/provider"; interface CustomReadOnlyEditorProps { initialValue?: string; @@ -21,6 +22,7 @@ interface CustomReadOnlyEditorProps { mentionHandler: { highlights: () => Promise; }; + provider?: HocuspocusProvider; } export const useReadOnlyEditor = ({ @@ -31,6 +33,7 @@ export const useReadOnlyEditor = ({ editorProps = {}, handleEditorReady, mentionHandler, + provider, }: CustomReadOnlyEditorProps) => { const editor = useCustomEditor({ editable: false, @@ -89,6 +92,14 @@ export const useReadOnlyEditor = ({ words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0, }; }, + emitRealTimeUpdate: (message: string) => { + if (provider) { + provider.sendStateless(message); + } + }, + listenToRealTimeUpdate: () => { + return provider; + }, })); if (!editor) { diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index f6c790305f3..9619aec9a35 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -25,6 +25,8 @@ export type EditorReadOnlyRefApi = { paragraphs: number; words: number; }; + emitRealTimeUpdate: (message: string) => void; + listenToRealTimeUpdate: () => any; }; export interface EditorRefApi extends EditorReadOnlyRefApi { diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 0560002d840..28f741102d6 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -2,6 +2,7 @@ import { observer } from "mobx-react"; import { useParams, useRouter } from "next/navigation"; +import { useState, useEffect } from "react"; import { ArchiveRestoreIcon, Clipboard, Copy, History, Link, Lock, LockOpen } from "lucide-react"; // document editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; @@ -23,6 +24,40 @@ type Props = { export const PageOptionsDropdown: React.FC = observer((props) => { const { editorRef, handleDuplicatePage, page } = props; + const [localAction, setLocalAction] = useState(null); + + useEffect(() => { + const provider = editorRef?.listenToRealTimeUpdate(); + + const handleStatelessMessage = (message: { payload: string }) => { + if (localAction === message.payload) { + setLocalAction(null); + return; + } + + switch (message.payload) { + case "locked": + handleLockPage(false); + break; + case "unlocked": + handleUnlockPage(false); + break; + case "archived": + handleArchivePage(false); + break; + case "unarchived": + handleRestorePage(false); + break; + } + }; + + provider?.on("stateless", handleStatelessMessage); + + return () => { + provider?.off("stateless", handleStatelessMessage); + }; + }, [editorRef, localAction]); + // router const router = useRouter(); // store values @@ -45,7 +80,11 @@ export const PageOptionsDropdown: React.FC = observer((props) => { // update query params const { updateQueryParams } = useQueryParams(); - const handleArchivePage = async () => + const handleArchivePage = async (isLocal: boolean = true) => { + if (isLocal) { + setLocalAction("archived"); + editorRef?.emitRealTimeUpdate("archive"); + } await archive().catch(() => setToast({ type: TOAST_TYPE.ERROR, @@ -53,8 +92,13 @@ export const PageOptionsDropdown: React.FC = observer((props) => { message: "Page could not be archived. Please try again later.", }) ); + }; - const handleRestorePage = async () => + const handleRestorePage = async (isLocal: boolean = true) => { + if (isLocal) { + setLocalAction("unarchived"); + editorRef?.emitRealTimeUpdate("unarchive"); + } await restore().catch(() => setToast({ type: TOAST_TYPE.ERROR, @@ -62,8 +106,13 @@ export const PageOptionsDropdown: React.FC = observer((props) => { message: "Page could not be restored. Please try again later.", }) ); + }; - const handleLockPage = async () => + const handleLockPage = async (isLocal: boolean = true) => { + if (isLocal) { + setLocalAction("locked"); + editorRef?.emitRealTimeUpdate("lock"); + } await lock().catch(() => setToast({ type: TOAST_TYPE.ERROR, @@ -71,8 +120,13 @@ export const PageOptionsDropdown: React.FC = observer((props) => { message: "Page could not be locked. Please try again later.", }) ); + }; - const handleUnlockPage = async () => + const handleUnlockPage = async (isLocal: boolean = true) => { + if (isLocal) { + setLocalAction("unlocked"); + editorRef?.emitRealTimeUpdate("unlock"); + } await unlock().catch(() => setToast({ type: TOAST_TYPE.ERROR, @@ -80,6 +134,7 @@ export const PageOptionsDropdown: React.FC = observer((props) => { message: "Page could not be unlocked. Please try again later.", }) ); + }; // menu items list const MENU_ITEMS: { @@ -132,14 +187,14 @@ export const PageOptionsDropdown: React.FC = observer((props) => { }, { key: "lock-unlock-page", - action: is_locked ? handleUnlockPage : handleLockPage, + action: is_locked ? () => handleUnlockPage() : () => handleLockPage(), label: is_locked ? "Unlock page" : "Lock page", icon: is_locked ? LockOpen : Lock, shouldRender: canCurrentUserLockPage, }, { key: "archive-restore-page", - action: archived_at ? handleRestorePage : handleArchivePage, + action: archived_at ? () => handleRestorePage() : () => handleArchivePage(), label: archived_at ? "Restore page" : "Archive page", icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon, shouldRender: canCurrentUserArchivePage, From cd2943023ad19c24216de4b9f9c2268fee7aea9d Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 17 Sep 2024 21:00:21 +0530 Subject: [PATCH 02/31] fix: show only after editor loads --- .../src/core/hooks/use-collaborative-editor.ts | 5 +++-- .../hooks/use-read-only-collaborative-editor.ts | 4 +++- packages/editor/src/core/types/collaboration.ts | 1 + web/core/components/pages/editor/editor-body.tsx | 13 ++++++++++++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 3947f1ee297..1adf1f6d5b1 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -30,6 +30,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { user, } = props; // initialize Hocuspocus provider + console.log("afdafd"); const provider = useMemo( () => new HocuspocusProvider({ @@ -44,8 +45,8 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { if (data.event.code === 1006) serverHandler?.onServerError?.(); }, onSynced: () => { - console.log("ran", id); - provider.sendStateless("Hello from client"); + console.log("ran"); + serverHandler?.onSynced?.(); }, }), [id, realtimeConfig, serverHandler, user.id] diff --git a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts index 63c7a68ab7d..11f7814e5bc 100644 --- a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts @@ -21,6 +21,7 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit user, } = props; // initialize Hocuspocus provider + console.log("afdafd read only"); const provider = useMemo( () => new HocuspocusProvider({ @@ -33,7 +34,8 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit if (data.event.code === 1006) serverHandler?.onServerError?.(); }, onSynced: () => { - console.log("ran", id); + console.log("ran"); + serverHandler?.onSynced?.(); }, }), [id, realtimeConfig, serverHandler, user.id] diff --git a/packages/editor/src/core/types/collaboration.ts b/packages/editor/src/core/types/collaboration.ts index 4b706a7f9f2..558f8a9447e 100644 --- a/packages/editor/src/core/types/collaboration.ts +++ b/packages/editor/src/core/types/collaboration.ts @@ -17,6 +17,7 @@ import { export type TServerHandler = { onConnect?: () => void; onServerError?: () => void; + onSynced?: () => void; }; type TCollaborativeEditorHookProps = { diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 6cd898c0079..9347e3f521e 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // document-editor @@ -60,6 +60,9 @@ export const PageEditorBody: React.FC = observer((props) => { sidePeekVisible, updateMarkings, } = props; + // states + const [isSynced, setIsSynced] = useState(false); + // router const { workspaceSlug, projectId } = useParams(); // store hooks @@ -108,10 +111,16 @@ export const PageEditorBody: React.FC = observer((props) => { handleConnectionStatus(true); }, []); + const handleServerSynced = useCallback(() => { + console.log("handleServerSynced called"); + setIsSynced(true); + }, []); + const serverHandler: TServerHandler = useMemo( () => ({ onConnect: handleServerConnect, onServerError: handleServerError, + onSynced: handleServerSynced, }), [] ); @@ -132,6 +141,8 @@ export const PageEditorBody: React.FC = observer((props) => { [projectId, workspaceSlug] ); + console.log("isSynced", isSynced); + if (pageId === undefined) return ; return ( From c94fff9d3d2d769ca3f9f180c3e6e2c42e4d3945 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Mon, 23 Sep 2024 19:49:27 +0530 Subject: [PATCH 03/31] fix: added strong types --- live/src/core/hocuspocus-server.ts | 20 ++++++++----------- .../src/core/helpers/document-events.ts | 9 +++++++++ .../core/hooks/use-collaborative-editor.ts | 3 --- packages/editor/src/core/hooks/use-editor.ts | 8 +++----- .../use-read-only-collaborative-editor.ts | 2 -- .../src/core/hooks/use-read-only-editor.ts | 1 - packages/editor/src/core/types/editor.ts | 6 ++++-- packages/editor/src/lib.ts | 1 + 8 files changed, 25 insertions(+), 25 deletions(-) create mode 100644 packages/editor/src/core/helpers/document-events.ts diff --git a/live/src/core/hocuspocus-server.ts b/live/src/core/hocuspocus-server.ts index e9fa03ccd1d..1e327e5e58e 100644 --- a/live/src/core/hocuspocus-server.ts +++ b/live/src/core/hocuspocus-server.ts @@ -4,6 +4,10 @@ import { v4 as uuidv4 } from "uuid"; import { handleAuthentication } from "@/core/lib/authentication.js"; // extensions import { getExtensions } from "@/core/extensions/index.js"; +import { + DocumentEventsServer, + documentEventResponses, +} from "@plane/editor/lib"; export const getHocusPocusServer = async () => { const extensions = await getExtensions(); @@ -38,20 +42,12 @@ export const getHocusPocusServer = async () => { } }, async onStateless({ payload, document }) { - if (payload === "lock") { - document.broadcastStateless("locked"); - } - if (payload === "unlock") { - document.broadcastStateless("unlocked"); - } - if (payload === "archive") { - document.broadcastStateless("archived"); - } - if (payload === "unarchive") { - document.broadcastStateless("unarchived"); + const response = documentEventResponses[payload as DocumentEventsServer]; + if (response) { + document.broadcastStateless(response); } }, extensions, - debounce: 10000 + debounce: 10000, }); }; diff --git a/packages/editor/src/core/helpers/document-events.ts b/packages/editor/src/core/helpers/document-events.ts new file mode 100644 index 00000000000..5746106f822 --- /dev/null +++ b/packages/editor/src/core/helpers/document-events.ts @@ -0,0 +1,9 @@ +export const documentEventResponses = { + lock: "locked", + unlock: "unlocked", + archive: "archived", + unarchive: "unarchived", +} as const; + +export type DocumentEventsServer = keyof typeof documentEventResponses; +export type DocumentEventsClient = (typeof documentEventResponses)[DocumentEventsServer]; diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 7715fe7ae0b..a0dd3ec2f58 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -30,7 +30,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { user, } = props; // initialize Hocuspocus provider - console.log("afdafd"); const provider = useMemo( () => new HocuspocusProvider({ @@ -45,7 +44,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { if (data.event.code === 1006) serverHandler?.onServerError?.(); }, onSynced: () => { - console.log("ran"); serverHandler?.onSynced?.(); }, }), @@ -96,7 +94,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { placeholder, provider, tabIndex, - provider, }); return { editor }; diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 923d7e1ac61..003828f26e6 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -17,7 +17,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; import { CoreEditorProps } from "@/props"; // types import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TEditorCommands, TFileHandler } from "@/types"; -import { HocuspocusProvider } from "@hocuspocus/provider"; +import { DocumentEventsServer } from "src/lib"; export interface CustomEditorProps { editorClassName: string; @@ -35,7 +35,6 @@ export interface CustomEditorProps { }; onChange?: (json: object, html: string) => void; placeholder?: string | ((isFocused: boolean, value: string) => string); - provider?: HocuspocusProvider; tabIndex?: number; // undefined when prop is not passed, null if intentionally passed to stop // swr syncing @@ -57,7 +56,6 @@ export const useEditor = (props: CustomEditorProps) => { mentionHandler, onChange, placeholder, - provider, tabIndex, value, provider, @@ -233,7 +231,7 @@ export const useEditor = (props: CustomEditorProps) => { if (empty) return null; const nodesArray: string[] = []; - state.doc.nodesBetween(from, to, (node, pos, parent) => { + state.doc.nodesBetween(from, to, (node, _pos, parent) => { if (parent === state.doc && editorRef.current) { const serializer = DOMSerializer.fromSchema(editorRef.current?.schema); const dom = serializer.serializeNode(node); @@ -274,7 +272,7 @@ export const useEditor = (props: CustomEditorProps) => { if (!document) return; Y.applyUpdate(document, value); }, - emitRealTimeUpdate: (message: string) => provider?.sendStateless(message), + emitRealTimeUpdate: (message: DocumentEventsServer) => provider?.sendStateless(message), listenToRealTimeUpdate: () => provider, }), [editorRef, savedSelection] diff --git a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts index 5af03edaa2f..4c34837be5e 100644 --- a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts @@ -21,7 +21,6 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit user, } = props; // initialize Hocuspocus provider - console.log("afdafd read only"); const provider = useMemo( () => new HocuspocusProvider({ @@ -34,7 +33,6 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit if (data.event.code === 1006) serverHandler?.onServerError?.(); }, onSynced: () => { - console.log("ran"); serverHandler?.onSynced?.(); }, }), diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index 7d63acab728..6fa56095d60 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -12,7 +12,6 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; import { CoreReadOnlyEditorProps } from "@/props"; // types import { EditorReadOnlyRefApi, IMentionHighlight } from "@/types"; -import { HocuspocusProvider } from "@hocuspocus/provider"; interface CustomReadOnlyEditorProps { initialValue?: string; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index 558c7eb2f21..7c13e3fdda6 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -13,6 +13,8 @@ import { TFileHandler, TServerHandler, } from "@/types"; +import { DocumentEventsServer } from "src/lib"; +import { HocuspocusProvider } from "@hocuspocus/provider"; // editor refs export type EditorReadOnlyRefApi = { @@ -30,8 +32,8 @@ export type EditorReadOnlyRefApi = { paragraphs: number; words: number; }; - emitRealTimeUpdate: (message: string) => void; - listenToRealTimeUpdate: () => any; + emitRealTimeUpdate: (message: DocumentEventsServer) => void; + listenToRealTimeUpdate: () => HocuspocusProvider; }; export interface EditorRefApi extends EditorReadOnlyRefApi { diff --git a/packages/editor/src/lib.ts b/packages/editor/src/lib.ts index e14c40127fb..270586e7c0c 100644 --- a/packages/editor/src/lib.ts +++ b/packages/editor/src/lib.ts @@ -1 +1,2 @@ export * from "@/extensions/core-without-props"; +export * from "@/helpers/document-events"; From 2c2dd62b7bb35b68544ff64d94a9238db6d4a967 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 24 Sep 2024 14:08:29 +0530 Subject: [PATCH 04/31] fix: live events fixed --- .../pages/editor/header/options-dropdown.tsx | 109 +++++++++++------- 1 file changed, 69 insertions(+), 40 deletions(-) diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 28f741102d6..5eb00a12a7f 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -6,6 +6,7 @@ import { useState, useEffect } from "react"; import { ArchiveRestoreIcon, Clipboard, Copy, History, Link, Lock, LockOpen } from "lucide-react"; // document editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; +import { DocumentEventsClient } from "@plane/editor/lib"; // ui import { ArchiveIcon, CustomMenu, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; // helpers @@ -24,16 +25,19 @@ type Props = { export const PageOptionsDropdown: React.FC = observer((props) => { const { editorRef, handleDuplicatePage, page } = props; + // create a local state to track if the current action is being processed const [localAction, setLocalAction] = useState(null); + // listen to real time updates from the live server useEffect(() => { const provider = editorRef?.listenToRealTimeUpdate(); - const handleStatelessMessage = (message: { payload: string }) => { + const handleStatelessMessage = (message: { payload: DocumentEventsClient }) => { if (localAction === message.payload) { setLocalAction(null); return; } + console.log("message", message.payload); switch (message.payload) { case "locked": @@ -81,59 +85,84 @@ export const PageOptionsDropdown: React.FC = observer((props) => { const { updateQueryParams } = useQueryParams(); const handleArchivePage = async (isLocal: boolean = true) => { - if (isLocal) { - setLocalAction("archived"); - editorRef?.emitRealTimeUpdate("archive"); - } - await archive().catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be archived. Please try again later.", + await archive() + .then(() => { + if (isLocal) { + setLocalAction("archived"); + console.log("archivinggg"); + } }) - ); + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Page could not be archived. Please try again later.", + }); + }); }; - const handleRestorePage = async (isLocal: boolean = true) => { - if (isLocal) { - setLocalAction("unarchived"); + // Add this useEffect hook to watch for changes in localAction + useEffect(() => { + if (localAction === "archived") { + editorRef?.emitRealTimeUpdate("archive"); + } + if (localAction === "unarchived") { editorRef?.emitRealTimeUpdate("unarchive"); } - await restore().catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be restored. Please try again later.", + if (localAction === "locked") { + editorRef?.emitRealTimeUpdate("lock"); + } + if (localAction === "unlocked") { + editorRef?.emitRealTimeUpdate("unlock"); + } + }, [localAction, editorRef]); + + const handleRestorePage = async (isLocal: boolean = true) => { + await restore() + .then(() => { + if (isLocal) { + setLocalAction("unarchived"); + } }) - ); + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Page could not be restored. Please try again later.", + }) + ); }; const handleLockPage = async (isLocal: boolean = true) => { - if (isLocal) { - setLocalAction("locked"); - editorRef?.emitRealTimeUpdate("lock"); - } - await lock().catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be locked. Please try again later.", + await lock() + .then(() => { + if (isLocal) { + setLocalAction("locked"); + } }) - ); + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Page could not be locked. Please try again later.", + }) + ); }; const handleUnlockPage = async (isLocal: boolean = true) => { - if (isLocal) { - setLocalAction("unlocked"); - editorRef?.emitRealTimeUpdate("unlock"); - } - await unlock().catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be unlocked. Please try again later.", + await unlock() + .then(() => { + if (isLocal) { + setLocalAction("unlocked"); + } }) - ); + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Page could not be unlocked. Please try again later.", + }) + ); }; // menu items list From c3221224a76b6e75a4a3412a253e1d6c9b74a2ec Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 24 Sep 2024 17:31:28 +0530 Subject: [PATCH 05/31] fix: remove unused vars and logs --- web/core/components/pages/editor/editor-body.tsx | 8 ++------ .../components/pages/editor/header/options-dropdown.tsx | 8 +++----- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 48f93abfdfc..9a228b32fec 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // document-editor @@ -7,7 +7,6 @@ import { CollaborativeDocumentReadOnlyEditorWithRef, EditorReadOnlyRefApi, EditorRefApi, - IMarking, TAIMenuProps, TDisplayConfig, TRealtimeConfig, @@ -70,7 +69,7 @@ export const PageEditorBody: React.FC = observer((props) => { project: { getProjectMemberIds }, } = useMember(); // derived values - const workspaceId = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "" : ""; + const workspaceId = workspaceSlug ? (getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "") : ""; const pageId = page?.id; const pageTitle = page?.name ?? ""; const { isContentEditable, updateTitle, setIsSubmitting } = page; @@ -109,7 +108,6 @@ export const PageEditorBody: React.FC = observer((props) => { }, []); const handleServerSynced = useCallback(() => { - console.log("handleServerSynced called"); setIsSynced(true); }, []); @@ -134,8 +132,6 @@ export const PageEditorBody: React.FC = observer((props) => { [projectId, workspaceSlug] ); - console.log("isSynced", isSynced); - if (pageId === undefined) return ; return ( diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 5eb00a12a7f..79e7ede49d4 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -37,7 +37,6 @@ export const PageOptionsDropdown: React.FC = observer((props) => { setLocalAction(null); return; } - console.log("message", message.payload); switch (message.payload) { case "locked": @@ -89,7 +88,6 @@ export const PageOptionsDropdown: React.FC = observer((props) => { .then(() => { if (isLocal) { setLocalAction("archived"); - console.log("archivinggg"); } }) .catch(() => { @@ -101,7 +99,7 @@ export const PageOptionsDropdown: React.FC = observer((props) => { }); }; - // Add this useEffect hook to watch for changes in localAction + // watch for changes in localAction useEffect(() => { if (localAction === "archived") { editorRef?.emitRealTimeUpdate("archive"); @@ -216,14 +214,14 @@ export const PageOptionsDropdown: React.FC = observer((props) => { }, { key: "lock-unlock-page", - action: is_locked ? () => handleUnlockPage() : () => handleLockPage(), + action: is_locked ? handleUnlockPage : handleLockPage, label: is_locked ? "Unlock page" : "Lock page", icon: is_locked ? LockOpen : Lock, shouldRender: canCurrentUserLockPage, }, { key: "archive-restore-page", - action: archived_at ? () => handleRestorePage() : () => handleArchivePage(), + action: archived_at ? handleRestorePage : handleArchivePage, label: archived_at ? "Restore page" : "Archive page", icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon, shouldRender: canCurrentUserArchivePage, From 83933ece5990743f05bdae08aaf7584e633461ed Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 3 Oct 2024 16:23:48 +0530 Subject: [PATCH 06/31] fix: converted objects to enum --- live/src/core/hocuspocus-server.ts | 4 +- .../src/core/helpers/document-events.ts | 16 +- packages/ui/src/icons/index.ts | 2 + .../pages/editor/header/options-dropdown.tsx | 206 +++++++++--------- 4 files changed, 121 insertions(+), 107 deletions(-) diff --git a/live/src/core/hocuspocus-server.ts b/live/src/core/hocuspocus-server.ts index 1e327e5e58e..ca2e4df0e6d 100644 --- a/live/src/core/hocuspocus-server.ts +++ b/live/src/core/hocuspocus-server.ts @@ -5,8 +5,8 @@ import { handleAuthentication } from "@/core/lib/authentication.js"; // extensions import { getExtensions } from "@/core/extensions/index.js"; import { + DocumentEventResponses, DocumentEventsServer, - documentEventResponses, } from "@plane/editor/lib"; export const getHocusPocusServer = async () => { @@ -42,7 +42,7 @@ export const getHocusPocusServer = async () => { } }, async onStateless({ payload, document }) { - const response = documentEventResponses[payload as DocumentEventsServer]; + const response = DocumentEventResponses[payload as DocumentEventsServer]; if (response) { document.broadcastStateless(response); } diff --git a/packages/editor/src/core/helpers/document-events.ts b/packages/editor/src/core/helpers/document-events.ts index 5746106f822..5ef356b8ac6 100644 --- a/packages/editor/src/core/helpers/document-events.ts +++ b/packages/editor/src/core/helpers/document-events.ts @@ -1,9 +1,9 @@ -export const documentEventResponses = { - lock: "locked", - unlock: "unlocked", - archive: "archived", - unarchive: "unarchived", -} as const; +export enum DocumentEventResponses { + Lock = "locked", + Unlock = "unlocked", + Archive = "archived", + Unarchive = "unarchived", +} -export type DocumentEventsServer = keyof typeof documentEventResponses; -export type DocumentEventsClient = (typeof documentEventResponses)[DocumentEventsServer]; +export type DocumentEventsServer = keyof typeof DocumentEventResponses; +export type DocumentEventsClient = (typeof DocumentEventResponses)[DocumentEventsServer]; diff --git a/packages/ui/src/icons/index.ts b/packages/ui/src/icons/index.ts index 660768845b3..584a6c3ddf5 100644 --- a/packages/ui/src/icons/index.ts +++ b/packages/ui/src/icons/index.ts @@ -28,3 +28,5 @@ export * from "./dropdown-icon"; export * from "./intake"; export * from "./user-activity-icon"; export * from "./favorite-folder-icon"; +// types +export type { ISvgIcons } from "./type"; diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 79e7ede49d4..abd6815a3ec 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -1,14 +1,14 @@ "use client"; +import { useState, useEffect, useCallback } from "react"; import { observer } from "mobx-react"; import { useParams, useRouter } from "next/navigation"; -import { useState, useEffect } from "react"; -import { ArchiveRestoreIcon, Clipboard, Copy, History, Link, Lock, LockOpen } from "lucide-react"; +import { ArchiveRestoreIcon, Clipboard, Copy, History, Link, Lock, LockOpen, LucideIcon } from "lucide-react"; // document editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; import { DocumentEventsClient } from "@plane/editor/lib"; // ui -import { ArchiveIcon, CustomMenu, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; +import { ArchiveIcon, CustomMenu, ISvgIcons, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; // helpers import { copyTextToClipboard, copyUrlToClipboard } from "@/helpers/string.helper"; // hooks @@ -28,39 +28,6 @@ export const PageOptionsDropdown: React.FC = observer((props) => { // create a local state to track if the current action is being processed const [localAction, setLocalAction] = useState(null); - // listen to real time updates from the live server - useEffect(() => { - const provider = editorRef?.listenToRealTimeUpdate(); - - const handleStatelessMessage = (message: { payload: DocumentEventsClient }) => { - if (localAction === message.payload) { - setLocalAction(null); - return; - } - - switch (message.payload) { - case "locked": - handleLockPage(false); - break; - case "unlocked": - handleUnlockPage(false); - break; - case "archived": - handleArchivePage(false); - break; - case "unarchived": - handleRestorePage(false); - break; - } - }; - - provider?.on("stateless", handleStatelessMessage); - - return () => { - provider?.off("stateless", handleStatelessMessage); - }; - }, [editorRef, localAction]); - // router const router = useRouter(); // store values @@ -83,92 +50,137 @@ export const PageOptionsDropdown: React.FC = observer((props) => { // update query params const { updateQueryParams } = useQueryParams(); - const handleArchivePage = async (isLocal: boolean = true) => { - await archive() - .then(() => { - if (isLocal) { - setLocalAction("archived"); - } - }) - .catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be archived. Please try again later.", + const handleArchivePage = useCallback( + async (isLocal: boolean = true) => { + await archive() + .then(() => { + if (isLocal) { + setLocalAction("archived"); + } + }) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Page could not be archived. Please try again later.", + }); }); - }); - }; + }, + [archive] + ); // watch for changes in localAction useEffect(() => { if (localAction === "archived") { - editorRef?.emitRealTimeUpdate("archive"); + editorRef?.emitRealTimeUpdate("Archive"); } if (localAction === "unarchived") { - editorRef?.emitRealTimeUpdate("unarchive"); + editorRef?.emitRealTimeUpdate("Unarchive"); } if (localAction === "locked") { - editorRef?.emitRealTimeUpdate("lock"); + editorRef?.emitRealTimeUpdate("Lock"); } if (localAction === "unlocked") { - editorRef?.emitRealTimeUpdate("unlock"); + editorRef?.emitRealTimeUpdate("Unlock"); } }, [localAction, editorRef]); - const handleRestorePage = async (isLocal: boolean = true) => { - await restore() - .then(() => { - if (isLocal) { - setLocalAction("unarchived"); - } - }) - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be restored. Please try again later.", + const handleRestorePage = useCallback( + async (isLocal: boolean = true) => { + await restore() + .then(() => { + if (isLocal) { + setLocalAction("unarchived"); + } }) - ); - }; + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Page could not be restored. Please try again later.", + }) + ); + }, + [restore] + ); - const handleLockPage = async (isLocal: boolean = true) => { - await lock() - .then(() => { - if (isLocal) { - setLocalAction("locked"); - } - }) - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be locked. Please try again later.", + const handleLockPage = useCallback( + async (isLocal: boolean = true) => { + await lock() + .then(() => { + if (isLocal) { + setLocalAction("locked"); + } }) - ); - }; + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Page could not be locked. Please try again later.", + }) + ); + }, + [lock] + ); - const handleUnlockPage = async (isLocal: boolean = true) => { - await unlock() - .then(() => { - if (isLocal) { - setLocalAction("unlocked"); - } - }) - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be unlocked. Please try again later.", + const handleUnlockPage = useCallback( + async (isLocal: boolean = true) => { + await unlock() + .then(() => { + if (isLocal) { + setLocalAction("unlocked"); + } }) - ); - }; + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Page could not be unlocked. Please try again later.", + }) + ); + }, + [unlock] + ); + + // listen to real time updates from the live server + useEffect(() => { + const provider = editorRef?.listenToRealTimeUpdate(); + + const handleStatelessMessage = (message: { payload: DocumentEventsClient }) => { + if (localAction === message.payload) { + setLocalAction(null); + return; + } + + switch (message.payload) { + case "locked": + handleLockPage(false); + break; + case "unlocked": + handleUnlockPage(false); + break; + case "archived": + handleArchivePage(false); + break; + case "unarchived": + handleRestorePage(false); + break; + } + }; + + provider?.on("stateless", handleStatelessMessage); + + return () => { + provider?.off("stateless", handleStatelessMessage); + }; + }, [editorRef, localAction, handleArchivePage, handleRestorePage, handleLockPage, handleUnlockPage]); // menu items list const MENU_ITEMS: { key: string; action: () => void; label: string; - icon: React.FC; + icon: LucideIcon | React.FC; shouldRender: boolean; }[] = [ { From 6f13c19294fcf247c11f6481e59ba932a6e36b8c Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 3 Oct 2024 16:24:19 +0530 Subject: [PATCH 07/31] fix: error handling and removing the events in read only mode --- .../custom-image/components/image-block.tsx | 23 ++++++++++++++++--- .../components/image-uploader.tsx | 5 ++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx index 42b51de5fb7..1301380e40a 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx @@ -71,6 +71,17 @@ export const CustomImageBlock: React.FC = (props) => { const containerRect = useRef(null); const imageRef = useRef(null); + const updateAttributesSafely = useCallback( + (attributes: Partial, errorMessage: string) => { + try { + updateAttributes(attributes); + } catch (error) { + console.error(`${errorMessage}:`, error); + } + }, + [updateAttributes] + ); + const handleImageLoad = useCallback(() => { const img = imageRef.current; if (!img) return; @@ -105,12 +116,18 @@ export const CustomImageBlock: React.FC = (props) => { }; setSize(initialComputedSize); - updateAttributes(initialComputedSize); + updateAttributesSafely( + initialComputedSize, + "Failed to update attributes while initializing an image for the first time:" + ); } else { // as the aspect ratio in not stored for old images, we need to update the attrs setSize((prevSize) => { const newSize = { ...prevSize, aspectRatio }; - updateAttributes(newSize); + updateAttributesSafely( + newSize, + "Failed to update attributes while initializing images with width but no aspect ratio:" + ); return newSize; }); } @@ -142,7 +159,7 @@ export const CustomImageBlock: React.FC = (props) => { const handleResizeEnd = useCallback(() => { setIsResizing(false); - updateAttributes(size); + updateAttributesSafely(size, "Failed to update attributes at the end of resizing:"); }, [size, updateAttributes]); const handleResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => { diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index 89cf36ca52b..0163ef08380 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -139,8 +139,9 @@ export const CustomImageUploader = (props: { return (
{ - if (!failedToLoadImage) { + if (!failedToLoadImage && editor.isEditable) { fileInputRef.current?.click(); } }} From 5a835e433a92aa058a480a6030c74e511e80aa54 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 3 Oct 2024 16:54:31 +0530 Subject: [PATCH 08/31] fix: added check to only update if the image aspect ratio is not present already --- .../custom-image/components/image-block.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx index 1301380e40a..8d5adff7d9b 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx @@ -114,7 +114,6 @@ export const CustomImageBlock: React.FC = (props) => { height: `${Math.round(initialHeight)}px` satisfies Pixel, aspectRatio: aspectRatio, }; - setSize(initialComputedSize); updateAttributesSafely( initialComputedSize, @@ -122,17 +121,19 @@ export const CustomImageBlock: React.FC = (props) => { ); } else { // as the aspect ratio in not stored for old images, we need to update the attrs - setSize((prevSize) => { - const newSize = { ...prevSize, aspectRatio }; - updateAttributesSafely( - newSize, - "Failed to update attributes while initializing images with width but no aspect ratio:" - ); - return newSize; - }); + if (!aspectRatio) { + setSize((prevSize) => { + const newSize = { ...prevSize, aspectRatio }; + updateAttributesSafely( + newSize, + "Failed to update attributes while initializing images with width but no aspect ratio:" + ); + return newSize; + }); + } } setInitialResizeComplete(true); - }, [width, updateAttributes, editorContainer]); + }, [width, updateAttributes, editorContainer, aspectRatio]); // for real time resizing useLayoutEffect(() => { From 1732587e60eee3c2e5f556273ff5116d8a69e187 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 3 Oct 2024 17:32:41 +0530 Subject: [PATCH 09/31] fix: imports --- packages/editor/src/core/helpers/document-events.ts | 3 --- packages/editor/src/core/hooks/use-editor.ts | 10 ++++++++-- packages/editor/src/core/types/document-events.ts | 4 ++++ packages/editor/src/core/types/editor.ts | 3 ++- 4 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 packages/editor/src/core/types/document-events.ts diff --git a/packages/editor/src/core/helpers/document-events.ts b/packages/editor/src/core/helpers/document-events.ts index 5ef356b8ac6..649471f1cd5 100644 --- a/packages/editor/src/core/helpers/document-events.ts +++ b/packages/editor/src/core/helpers/document-events.ts @@ -4,6 +4,3 @@ export enum DocumentEventResponses { Archive = "archived", Unarchive = "unarchived", } - -export type DocumentEventsServer = keyof typeof DocumentEventResponses; -export type DocumentEventsClient = (typeof DocumentEventResponses)[DocumentEventsServer]; diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index b280c9f6aab..b69afb45000 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -16,8 +16,14 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; // props import { CoreEditorProps } from "@/props"; // types -import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TEditorCommands, TFileHandler } from "@/types"; -import { DocumentEventsServer } from "src/lib"; +import { + DocumentEventsServer, + EditorRefApi, + IMentionHighlight, + IMentionSuggestion, + TEditorCommands, + TFileHandler, +} from "@/types"; export interface CustomEditorProps { editorClassName: string; diff --git a/packages/editor/src/core/types/document-events.ts b/packages/editor/src/core/types/document-events.ts new file mode 100644 index 00000000000..8c7d7b118f3 --- /dev/null +++ b/packages/editor/src/core/types/document-events.ts @@ -0,0 +1,4 @@ +import { DocumentEventResponses } from "@/helpers/document-events"; + +export type DocumentEventsServer = keyof typeof DocumentEventResponses; +export type DocumentEventsClient = (typeof DocumentEventResponses)[DocumentEventsServer]; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index 7c13e3fdda6..8edd3c8cbfe 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -12,8 +12,9 @@ import { TExtensions, TFileHandler, TServerHandler, + DocumentEventsServer, } from "@/types"; -import { DocumentEventsServer } from "src/lib"; + import { HocuspocusProvider } from "@hocuspocus/provider"; // editor refs From 20861a6d6d293fedb5ace7a7c4b0def2a4f56212 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 3 Oct 2024 17:33:42 +0530 Subject: [PATCH 10/31] fix: props order --- packages/editor/src/core/hooks/use-editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index b69afb45000..8f0975c4c50 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -41,11 +41,11 @@ export interface CustomEditorProps { }; onChange?: (json: object, html: string) => void; placeholder?: string | ((isFocused: boolean, value: string) => string); + provider?: HocuspocusProvider; tabIndex?: number; // undefined when prop is not passed, null if intentionally passed to stop // swr syncing value?: string | null | undefined; - provider?: HocuspocusProvider; } export const useEditor = (props: CustomEditorProps) => { From 2384f0bed83d925881ec39782d76aad94f7878e0 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 3 Oct 2024 17:38:16 +0530 Subject: [PATCH 11/31] revert: no need of these changes anymore --- web/core/components/pages/editor/editor-body.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 9daab78b8ec..cfd761fae7a 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -56,9 +56,6 @@ export const PageEditorBody: React.FC = observer((props) => { readOnlyEditorRef, sidePeekVisible, } = props; - // states - const [isSynced, setIsSynced] = useState(false); - // router const { workspaceSlug, projectId } = useParams(); // store hooks @@ -107,17 +104,12 @@ export const PageEditorBody: React.FC = observer((props) => { handleConnectionStatus(true); }, []); - const handleServerSynced = useCallback(() => { - setIsSynced(true); - }, []); - const serverHandler: TServerHandler = useMemo( () => ({ onConnect: handleServerConnect, onServerError: handleServerError, - onSynced: handleServerSynced, }), - [] + [handleServerConnect, handleServerError] ); const realtimeConfig: TRealtimeConfig | undefined = useMemo(() => { From bbe7e6226af512e7dec751c944e3208fc36e26ea Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 3 Oct 2024 17:52:20 +0530 Subject: [PATCH 12/31] fix: updated type names --- live/src/core/hocuspocus-server.ts | 4 ++-- packages/editor/src/core/hooks/use-editor.ts | 6 +++--- packages/editor/src/core/types/document-events.ts | 4 ++-- packages/editor/src/core/types/editor.ts | 4 ++-- packages/editor/src/core/types/index.ts | 1 + packages/editor/src/lib.ts | 1 + packages/ui/src/icons/index.ts | 3 ++- .../components/pages/editor/header/options-dropdown.tsx | 4 ++-- 8 files changed, 15 insertions(+), 12 deletions(-) diff --git a/live/src/core/hocuspocus-server.ts b/live/src/core/hocuspocus-server.ts index ca2e4df0e6d..86b6b10ac18 100644 --- a/live/src/core/hocuspocus-server.ts +++ b/live/src/core/hocuspocus-server.ts @@ -6,7 +6,7 @@ import { handleAuthentication } from "@/core/lib/authentication.js"; import { getExtensions } from "@/core/extensions/index.js"; import { DocumentEventResponses, - DocumentEventsServer, + TDocumentEventsServer, } from "@plane/editor/lib"; export const getHocusPocusServer = async () => { @@ -42,7 +42,7 @@ export const getHocusPocusServer = async () => { } }, async onStateless({ payload, document }) { - const response = DocumentEventResponses[payload as DocumentEventsServer]; + const response = DocumentEventResponses[payload as TDocumentEventsServer]; if (response) { document.broadcastStateless(response); } diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 8f0975c4c50..c1845fb3d06 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -16,8 +16,8 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; // props import { CoreEditorProps } from "@/props"; // types -import { - DocumentEventsServer, +import type { + TDocumentEventsServer, EditorRefApi, IMentionHighlight, IMentionSuggestion, @@ -279,7 +279,7 @@ export const useEditor = (props: CustomEditorProps) => { if (!document) return; Y.applyUpdate(document, value); }, - emitRealTimeUpdate: (message: DocumentEventsServer) => provider?.sendStateless(message), + emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message), listenToRealTimeUpdate: () => provider, }), [editorRef, savedSelection] diff --git a/packages/editor/src/core/types/document-events.ts b/packages/editor/src/core/types/document-events.ts index 8c7d7b118f3..bd9b5d5e31b 100644 --- a/packages/editor/src/core/types/document-events.ts +++ b/packages/editor/src/core/types/document-events.ts @@ -1,4 +1,4 @@ import { DocumentEventResponses } from "@/helpers/document-events"; -export type DocumentEventsServer = keyof typeof DocumentEventResponses; -export type DocumentEventsClient = (typeof DocumentEventResponses)[DocumentEventsServer]; +export type TDocumentEventsServer = keyof typeof DocumentEventResponses; +export type TDocumentEventsClient = (typeof DocumentEventResponses)[TDocumentEventsServer]; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index 8edd3c8cbfe..495d29d9464 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -12,7 +12,7 @@ import { TExtensions, TFileHandler, TServerHandler, - DocumentEventsServer, + TDocumentEventsServer, } from "@/types"; import { HocuspocusProvider } from "@hocuspocus/provider"; @@ -33,7 +33,7 @@ export type EditorReadOnlyRefApi = { paragraphs: number; words: number; }; - emitRealTimeUpdate: (message: DocumentEventsServer) => void; + emitRealTimeUpdate: (message: TDocumentEventsServer) => void; listenToRealTimeUpdate: () => HocuspocusProvider; }; diff --git a/packages/editor/src/core/types/index.ts b/packages/editor/src/core/types/index.ts index 8da9ed276e5..1e5a276463d 100644 --- a/packages/editor/src/core/types/index.ts +++ b/packages/editor/src/core/types/index.ts @@ -8,3 +8,4 @@ export * from "./image"; export * from "./mention-suggestion"; export * from "./slash-commands-suggestion"; export * from "@/plane-editor/types"; +export * from "./document-events"; diff --git a/packages/editor/src/lib.ts b/packages/editor/src/lib.ts index 270586e7c0c..a2e3bb2328e 100644 --- a/packages/editor/src/lib.ts +++ b/packages/editor/src/lib.ts @@ -1,2 +1,3 @@ export * from "@/extensions/core-without-props"; export * from "@/helpers/document-events"; +export * from "@/types/document-events"; diff --git a/packages/ui/src/icons/index.ts b/packages/ui/src/icons/index.ts index 99ca67400f8..d16e104465a 100644 --- a/packages/ui/src/icons/index.ts +++ b/packages/ui/src/icons/index.ts @@ -31,4 +31,5 @@ export * from "./user-activity-icon"; export * from "./favorite-folder-icon"; export * from "./planned-icon"; export * from "./in-progress-icon"; -export * from "./done-icon"; \ No newline at end of file +export * from "./done-icon"; + diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index abd6815a3ec..dfaa60ea51c 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -6,7 +6,7 @@ import { useParams, useRouter } from "next/navigation"; import { ArchiveRestoreIcon, Clipboard, Copy, History, Link, Lock, LockOpen, LucideIcon } from "lucide-react"; // document editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; -import { DocumentEventsClient } from "@plane/editor/lib"; +import { TDocumentEventsClient } from "@plane/editor/lib"; // ui import { ArchiveIcon, CustomMenu, ISvgIcons, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; // helpers @@ -146,7 +146,7 @@ export const PageOptionsDropdown: React.FC = observer((props) => { useEffect(() => { const provider = editorRef?.listenToRealTimeUpdate(); - const handleStatelessMessage = (message: { payload: DocumentEventsClient }) => { + const handleStatelessMessage = (message: { payload: TDocumentEventsClient }) => { if (localAction === message.payload) { setLocalAction(null); return; From b0bf24262cfe48003e708018a92510fae94c8371 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 3 Oct 2024 18:17:59 +0530 Subject: [PATCH 13/31] fix: order of things --- .../pages/editor/header/options-dropdown.tsx | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index dfaa60ea51c..117244d8b23 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -25,7 +25,8 @@ type Props = { export const PageOptionsDropdown: React.FC = observer((props) => { const { editorRef, handleDuplicatePage, page } = props; - // create a local state to track if the current action is being processed + // create a local state to track if the current action is being processed, a + // local action is by the client const [localAction, setLocalAction] = useState(null); // router @@ -50,6 +51,21 @@ export const PageOptionsDropdown: React.FC = observer((props) => { // update query params const { updateQueryParams } = useQueryParams(); + useEffect(() => { + if (localAction === "archived") { + editorRef?.emitRealTimeUpdate("Archive"); + } + if (localAction === "unarchived") { + editorRef?.emitRealTimeUpdate("Unarchive"); + } + if (localAction === "locked") { + editorRef?.emitRealTimeUpdate("Lock"); + } + if (localAction === "unlocked") { + editorRef?.emitRealTimeUpdate("Unlock"); + } + }, [localAction, editorRef]); + const handleArchivePage = useCallback( async (isLocal: boolean = true) => { await archive() @@ -69,22 +85,6 @@ export const PageOptionsDropdown: React.FC = observer((props) => { [archive] ); - // watch for changes in localAction - useEffect(() => { - if (localAction === "archived") { - editorRef?.emitRealTimeUpdate("Archive"); - } - if (localAction === "unarchived") { - editorRef?.emitRealTimeUpdate("Unarchive"); - } - if (localAction === "locked") { - editorRef?.emitRealTimeUpdate("Lock"); - } - if (localAction === "unlocked") { - editorRef?.emitRealTimeUpdate("Unlock"); - } - }, [localAction, editorRef]); - const handleRestorePage = useCallback( async (isLocal: boolean = true) => { await restore() From b42f55281ed60218604b237e1b6f7dcef4bf2d7d Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 8 Oct 2024 17:37:59 +0530 Subject: [PATCH 14/31] fix: fixed types and renamed variables --- .../src/core/hooks/use-read-only-editor.ts | 12 +--- .../editor/src/core/types/collaboration.ts | 1 - .../pages/editor/header/options-dropdown.tsx | 63 +++++++++++-------- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index 8f2ea103752..b89efcda59e 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -11,7 +11,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; // props import { CoreReadOnlyEditorProps } from "@/props"; // types -import { EditorReadOnlyRefApi, IMentionHighlight } from "@/types"; +import { EditorReadOnlyRefApi, IMentionHighlight, TDocumentEventsServer } from "@/types"; interface CustomReadOnlyEditorProps { initialValue?: string; @@ -112,14 +112,8 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { editorRef.current?.off("update"); }; }, - emitRealTimeUpdate: (message: string) => { - if (provider) { - provider.sendStateless(message); - } - }, - listenToRealTimeUpdate: () => { - return provider; - }, + emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message), + listenToRealTimeUpdate: () => provider, getHeadings: () => editorRef?.current?.storage.headingList.headings, })); diff --git a/packages/editor/src/core/types/collaboration.ts b/packages/editor/src/core/types/collaboration.ts index 558f8a9447e..4b706a7f9f2 100644 --- a/packages/editor/src/core/types/collaboration.ts +++ b/packages/editor/src/core/types/collaboration.ts @@ -17,7 +17,6 @@ import { export type TServerHandler = { onConnect?: () => void; onServerError?: () => void; - onSynced?: () => void; }; type TCollaborativeEditorHookProps = { diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 70e43b6e3c6..91559091a05 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -1,9 +1,19 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback } from "react"; import { observer } from "mobx-react"; import { useParams, useRouter } from "next/navigation"; -import { ArchiveRestoreIcon, ArrowUpToLine, Clipboard, Copy, History, Link, Lock, LockOpen } from "lucide-react"; +import { + ArchiveRestoreIcon, + ArrowUpToLine, + Clipboard, + Copy, + History, + Link, + Lock, + LockOpen, + LucideIcon, +} from "lucide-react"; // document editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; import { TDocumentEventsClient } from "@plane/editor/lib"; @@ -27,10 +37,6 @@ type Props = { export const PageOptionsDropdown: React.FC = observer((props) => { const { editorRef, handleDuplicatePage, page } = props; - // create a local state to track if the current action is being processed, a - // local action is by the client - const [localAction, setLocalAction] = useState(null); - // router const router = useRouter(); // store values @@ -49,6 +55,9 @@ export const PageOptionsDropdown: React.FC = observer((props) => { } = page; // states const [isExportModalOpen, setIsExportModalOpen] = useState(false); + // currentUserAction local state to track if the current action is being processed, a + // local action is basically the action performed by the current user to avoid double operations + const [currentUserAction, setCurrentUserAction] = useState(null); // store hooks const { workspaceSlug, projectId } = useParams(); // page filters @@ -57,26 +66,26 @@ export const PageOptionsDropdown: React.FC = observer((props) => { const { updateQueryParams } = useQueryParams(); useEffect(() => { - if (localAction === "archived") { + if (currentUserAction === "archived") { editorRef?.emitRealTimeUpdate("Archive"); } - if (localAction === "unarchived") { + if (currentUserAction === "unarchived") { editorRef?.emitRealTimeUpdate("Unarchive"); } - if (localAction === "locked") { + if (currentUserAction === "locked") { editorRef?.emitRealTimeUpdate("Lock"); } - if (localAction === "unlocked") { + if (currentUserAction === "unlocked") { editorRef?.emitRealTimeUpdate("Unlock"); } - }, [localAction, editorRef]); + }, [currentUserAction, editorRef]); const handleArchivePage = useCallback( - async (isLocal: boolean = true) => { + async (isPerformedByCurrentUser: boolean = true) => { await archive() .then(() => { - if (isLocal) { - setLocalAction("archived"); + if (isPerformedByCurrentUser) { + setCurrentUserAction("archived"); } }) .catch(() => { @@ -91,11 +100,11 @@ export const PageOptionsDropdown: React.FC = observer((props) => { ); const handleRestorePage = useCallback( - async (isLocal: boolean = true) => { + async (isPerformedByCurrentUser: boolean = true) => { await restore() .then(() => { - if (isLocal) { - setLocalAction("unarchived"); + if (isPerformedByCurrentUser) { + setCurrentUserAction("unarchived"); } }) .catch(() => @@ -110,11 +119,11 @@ export const PageOptionsDropdown: React.FC = observer((props) => { ); const handleLockPage = useCallback( - async (isLocal: boolean = true) => { + async (isPerformedByCurrentUser: boolean = true) => { await lock() .then(() => { - if (isLocal) { - setLocalAction("locked"); + if (isPerformedByCurrentUser) { + setCurrentUserAction("locked"); } }) .catch(() => @@ -129,11 +138,11 @@ export const PageOptionsDropdown: React.FC = observer((props) => { ); const handleUnlockPage = useCallback( - async (isLocal: boolean = true) => { + async (isPerformedByCurrentUser: boolean = true) => { await unlock() .then(() => { - if (isLocal) { - setLocalAction("unlocked"); + if (isPerformedByCurrentUser) { + setCurrentUserAction("unlocked"); } }) .catch(() => @@ -152,8 +161,8 @@ export const PageOptionsDropdown: React.FC = observer((props) => { const provider = editorRef?.listenToRealTimeUpdate(); const handleStatelessMessage = (message: { payload: TDocumentEventsClient }) => { - if (localAction === message.payload) { - setLocalAction(null); + if (currentUserAction === message.payload) { + setCurrentUserAction(null); return; } @@ -178,7 +187,7 @@ export const PageOptionsDropdown: React.FC = observer((props) => { return () => { provider?.off("stateless", handleStatelessMessage); }; - }, [editorRef, localAction, handleArchivePage, handleRestorePage, handleLockPage, handleUnlockPage]); + }, [editorRef, currentUserAction, handleArchivePage, handleRestorePage, handleLockPage, handleUnlockPage]); // menu items list const MENU_ITEMS: { @@ -293,4 +302,4 @@ export const PageOptionsDropdown: React.FC = observer((props) => { ); -}); \ No newline at end of file +}); From 702236e54920474be1e0025b3cf0e73ea4324fe8 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 8 Oct 2024 18:28:41 +0530 Subject: [PATCH 15/31] fix: better typing for the real time updates --- .../editor/src/core/types/document-events.ts | 2 +- .../pages/editor/header/options-dropdown.tsx | 34 ++++++++++--------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/editor/src/core/types/document-events.ts b/packages/editor/src/core/types/document-events.ts index bd9b5d5e31b..8bfeaab3537 100644 --- a/packages/editor/src/core/types/document-events.ts +++ b/packages/editor/src/core/types/document-events.ts @@ -1,4 +1,4 @@ import { DocumentEventResponses } from "@/helpers/document-events"; export type TDocumentEventsServer = keyof typeof DocumentEventResponses; -export type TDocumentEventsClient = (typeof DocumentEventResponses)[TDocumentEventsServer]; +export type TDocumentEventsClient = `${DocumentEventResponses}`; diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 91559091a05..fc760641331 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -65,21 +65,6 @@ export const PageOptionsDropdown: React.FC = observer((props) => { // update query params const { updateQueryParams } = useQueryParams(); - useEffect(() => { - if (currentUserAction === "archived") { - editorRef?.emitRealTimeUpdate("Archive"); - } - if (currentUserAction === "unarchived") { - editorRef?.emitRealTimeUpdate("Unarchive"); - } - if (currentUserAction === "locked") { - editorRef?.emitRealTimeUpdate("Lock"); - } - if (currentUserAction === "unlocked") { - editorRef?.emitRealTimeUpdate("Unlock"); - } - }, [currentUserAction, editorRef]); - const handleArchivePage = useCallback( async (isPerformedByCurrentUser: boolean = true) => { await archive() @@ -156,7 +141,24 @@ export const PageOptionsDropdown: React.FC = observer((props) => { [unlock] ); - // listen to real time updates from the live server + // this is for the emitting real time updates for the current user's action + useEffect(() => { + if (currentUserAction === "archived") { + editorRef?.emitRealTimeUpdate("Archive"); + } + if (currentUserAction === "unarchived") { + editorRef?.emitRealTimeUpdate("Unarchive"); + } + if (currentUserAction === "locked") { + editorRef?.emitRealTimeUpdate("Lock"); + } + if (currentUserAction === "unlocked") { + editorRef?.emitRealTimeUpdate("Unlock"); + } + }, [currentUserAction, editorRef]); + + // this is for listening to real time updates from the live server for remote + // users' actions useEffect(() => { const provider = editorRef?.listenToRealTimeUpdate(); From ee9e7f5bf3a02ddbfcdb1a324debcc209f5627e9 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 8 Oct 2024 20:50:24 +0530 Subject: [PATCH 16/31] fix: trying multiplexing our socket connection --- .../editors/document/collaborative-editor.tsx | 2 ++ .../core/hooks/use-collaborative-editor.ts | 25 +++++++++-------- .../editor/src/core/types/collaboration.ts | 2 ++ packages/editor/src/core/types/editor.ts | 3 ++- .../pages/(detail)/[pageId]/page.tsx | 1 + .../components/pages/editor/editor-body.tsx | 6 +++++ web/core/components/pages/editor/socket.ts | 27 +++++++++++++++++++ 7 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 web/core/components/pages/editor/socket.ts diff --git a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx index a008d5c60ba..1c09863f224 100644 --- a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx +++ b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx @@ -14,6 +14,7 @@ import { EditorRefApi, ICollaborativeDocumentEditor } from "@/types"; const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => { const { + socket, aiHandler, containerClassName, disabledExtensions, @@ -47,6 +48,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => { editorClassName, embedHandler, extensions, + socket, fileHandler, forwardedRef, handleEditorReady, diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 5a004bff284..ae24cbc07db 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useMemo, useState } from "react"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { HocuspocusProvider } from "@hocuspocus/provider"; import Collaboration from "@tiptap/extension-collaboration"; import { IndexeddbPersistence } from "y-indexeddb"; @@ -28,6 +28,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { serverHandler, tabIndex, user, + socket, } = props; // states const [hasServerConnectionFailed, setHasServerConnectionFailed] = useState(false); @@ -36,11 +37,12 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { const provider = useMemo( () => new HocuspocusProvider({ + websocketProvider: socket, name: id, - parameters: realtimeConfig.queryParams, + // parameters: realtimeConfig.queryParams, // using user id as a token to verify the user on the server token: user.id, - url: realtimeConfig.url, + // url: realtimeConfig.url, onAuthenticationFailed: () => { serverHandler?.onServerError?.(); setHasServerConnectionFailed(true); @@ -57,14 +59,15 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { [id, realtimeConfig, serverHandler, user.id] ); - // destroy and disconnect connection on unmount - useEffect( - () => () => { - provider.destroy(); - provider.disconnect(); - }, - [provider] - ); + // // destroy and disconnect connection on unmount + // useEffect( + // () => () => { + // provider.destroy(); + // provider.disconnect(); + // }, + // [provider] + // ); + // indexed db integration for offline support useLayoutEffect(() => { const localProvider = new IndexeddbPersistence(id, provider.document); diff --git a/packages/editor/src/core/types/collaboration.ts b/packages/editor/src/core/types/collaboration.ts index 4b706a7f9f2..dc3bc1ddaf4 100644 --- a/packages/editor/src/core/types/collaboration.ts +++ b/packages/editor/src/core/types/collaboration.ts @@ -13,6 +13,7 @@ import { TRealtimeConfig, TUserDetails, } from "@/types"; +import { HocuspocusProviderWebsocket } from "@hocuspocus/provider"; export type TServerHandler = { onConnect?: () => void; @@ -41,6 +42,7 @@ export type TCollaborativeEditorProps = TCollaborativeEditorHookProps & { forwardedRef?: React.MutableRefObject; placeholder?: string | ((isFocused: boolean, value: string) => string); tabIndex?: number; + socket?: HocuspocusProviderWebsocket; }; export type TReadOnlyCollaborativeEditorProps = TCollaborativeEditorHookProps & { diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index 495d29d9464..bc6c80e5414 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -15,7 +15,7 @@ import { TDocumentEventsServer, } from "@/types"; -import { HocuspocusProvider } from "@hocuspocus/provider"; +import { HocuspocusProvider, HocuspocusProviderWebsocket } from "@hocuspocus/provider"; // editor refs export type EditorReadOnlyRefApi = { @@ -85,6 +85,7 @@ export interface ICollaborativeDocumentEditor realtimeConfig: TRealtimeConfig; serverHandler?: TServerHandler; user: TUserDetails; + socket: HocuspocusProviderWebsocket; } // read only editor props diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx index e9debb2bcf4..2d9182d3831 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { HocuspocusProviderWebsocket } from "@hocuspocus/provider"; import { observer } from "mobx-react"; import Link from "next/link"; import { useParams } from "next/navigation"; diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index cfd761fae7a..9659a7bbb4c 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -1,4 +1,5 @@ import { useCallback, useMemo, useState } from "react"; +import { HocuspocusProviderWebsocket } from "@hocuspocus/provider"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // document-editor @@ -32,6 +33,7 @@ import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed"; import { FileService } from "@/services/file.service"; // store import { IPage } from "@/store/pages/page"; +import { getSocketConnection } from "./socket"; const fileService = new FileService(); @@ -137,6 +139,9 @@ export const PageEditorBody: React.FC = observer((props) => { if (pageId === undefined || !realtimeConfig) return ; + const socket = useMemo(() => getSocketConnection(realtimeConfig, currentUser?.id), [realtimeConfig]); + console.log("socket connection", socket); + return (
= observer((props) => {
{isContentEditable ? ( keyA.localeCompare(keyB)); + return JSON.stringify(sortedEntries); +} + +const socketUrlMap = new Map(); + +export const getSocketConnection = (realtimeConfig: TRealtimeConfig) => { + const configKey = stringifyConfig(realtimeConfig); + + console.log("socketUrlMap", socketUrlMap); + if (socketUrlMap.has(configKey)) { + console.log("existing socket returned"); + return socketUrlMap.get(configKey); + } else { + console.log("new socket returned"); + const socket = new HocuspocusProviderWebsocket({ + url: realtimeConfig.url, + parameters: realtimeConfig.queryParams, + }); + socketUrlMap.set(configKey, socket); + return socket; + } +}; From 9f0ca0d6ff1f0d0a19d6a01f064a162c9aa345f6 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 7 Nov 2024 11:15:17 +0530 Subject: [PATCH 17/31] fix: multiplexing socket connection in read only editor as well --- .../collaborative-read-only-editor.tsx | 2 ++ .../core/hooks/use-collaborative-editor.ts | 23 ++++++++++--------- .../use-read-only-collaborative-editor.ts | 13 +++++++---- .../editor/src/core/types/collaboration.ts | 1 + packages/editor/src/core/types/editor.ts | 1 + .../components/pages/editor/editor-body.tsx | 10 ++++---- 6 files changed, 31 insertions(+), 19 deletions(-) diff --git a/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx index aa925abece4..0c92b57ff91 100644 --- a/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx +++ b/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx @@ -14,6 +14,7 @@ import { EditorReadOnlyRefApi, ICollaborativeDocumentReadOnlyEditor } from "@/ty const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOnlyEditor) => { const { + socket, containerClassName, displayConfig = DEFAULT_DISPLAY_CONFIG, editorClassName = "", @@ -39,6 +40,7 @@ const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOn const { editor, hasServerConnectionFailed, hasServerSynced } = useReadOnlyCollaborativeEditor({ editorClassName, extensions, + socket, fileHandler, forwardedRef, handleEditorReady, diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index ae24cbc07db..b31e8a54ada 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -39,10 +39,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { new HocuspocusProvider({ websocketProvider: socket, name: id, - // parameters: realtimeConfig.queryParams, - // using user id as a token to verify the user on the server token: user.id, - // url: realtimeConfig.url, onAuthenticationFailed: () => { serverHandler?.onServerError?.(); setHasServerConnectionFailed(true); @@ -54,19 +51,23 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { setHasServerConnectionFailed(true); } }, + preserveConnection: true, onSynced: () => setHasServerSynced(true), }), [id, realtimeConfig, serverHandler, user.id] ); - // // destroy and disconnect connection on unmount - // useEffect( - // () => () => { - // provider.destroy(); - // provider.disconnect(); - // }, - // [provider] - // ); + // destroy and disconnect connection on unmount + useEffect( + () => () => { + setTimeout(() => { + console.log("destroying provider", id); + provider.destroy(); + }, 4000); + // provider.destroy(); + }, + [] + ); // indexed db integration for offline support useLayoutEffect(() => { diff --git a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts index 7e8de6bdfde..5d63178cdf4 100644 --- a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts @@ -22,6 +22,7 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit realtimeConfig, serverHandler, user, + socket, } = props; // states const [hasServerConnectionFailed, setHasServerConnectionFailed] = useState(false); @@ -30,10 +31,9 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit const provider = useMemo( () => new HocuspocusProvider({ - url: realtimeConfig.url, + websocketProvider: socket, name: id, token: user.id, - parameters: realtimeConfig.queryParams, onAuthenticationFailed: () => { serverHandler?.onServerError?.(); setHasServerConnectionFailed(true); @@ -45,6 +45,7 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit setHasServerConnectionFailed(true); } }, + preserveConnection: true, onSynced: () => setHasServerSynced(true), }), [id, realtimeConfig, serverHandler, user.id] @@ -53,11 +54,15 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit // destroy and disconnect connection on unmount useEffect( () => () => { - provider.destroy(); - provider.disconnect(); + setTimeout(() => { + console.log("destroying read only provider", id); + provider.destroy(); + }, 4000); + // provider.destroy(); }, [provider] ); + // indexed db integration for offline support useLayoutEffect(() => { const localProvider = new IndexeddbPersistence(id, provider.document); diff --git a/packages/editor/src/core/types/collaboration.ts b/packages/editor/src/core/types/collaboration.ts index 062c70dcee8..dce347f6cde 100644 --- a/packages/editor/src/core/types/collaboration.ts +++ b/packages/editor/src/core/types/collaboration.ts @@ -48,4 +48,5 @@ export type TCollaborativeEditorProps = TCollaborativeEditorHookProps & { export type TReadOnlyCollaborativeEditorProps = TCollaborativeEditorHookProps & { fileHandler: Pick; forwardedRef?: React.MutableRefObject; + socket?: HocuspocusProviderWebsocket; }; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index fe0be0cf57a..faa2bf392ab 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -132,6 +132,7 @@ export interface ICollaborativeDocumentReadOnlyEditor extends Omit = observer((props) => { if (pageId === undefined || !realtimeConfig) return ; const socket = useMemo(() => getSocketConnection(realtimeConfig, currentUser?.id), [realtimeConfig]); - console.log("socket connection", socket); + + if (!socket) return ; + // console.log("socket connection", socket); return (
@@ -186,7 +187,7 @@ export const PageEditorBody: React.FC = observer((props) => {
{isContentEditable ? ( = observer((props) => { /> ) : ( Date: Mon, 18 Nov 2024 13:48:23 +0530 Subject: [PATCH 18/31] fix: remove single socket logic --- .../collaborative-read-only-editor.tsx | 2 - .../core/hooks/use-collaborative-editor.ts | 2 - .../use-read-only-collaborative-editor.ts | 3 +- .../components/pages/editor/editor-body.tsx | 7 - yarn.lock | 1201 ++++++++++++++++- 5 files changed, 1168 insertions(+), 47 deletions(-) diff --git a/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx index 0c92b57ff91..aa925abece4 100644 --- a/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx +++ b/packages/editor/src/core/components/editors/document/collaborative-read-only-editor.tsx @@ -14,7 +14,6 @@ import { EditorReadOnlyRefApi, ICollaborativeDocumentReadOnlyEditor } from "@/ty const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOnlyEditor) => { const { - socket, containerClassName, displayConfig = DEFAULT_DISPLAY_CONFIG, editorClassName = "", @@ -40,7 +39,6 @@ const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOn const { editor, hasServerConnectionFailed, hasServerSynced } = useReadOnlyCollaborativeEditor({ editorClassName, extensions, - socket, fileHandler, forwardedRef, handleEditorReady, diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 223c8eca679..38af11ccfe6 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -29,7 +29,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { serverHandler, tabIndex, user, - socket, } = props; // states const [hasServerConnectionFailed, setHasServerConnectionFailed] = useState(false); @@ -38,7 +37,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { const provider = useMemo( () => new HocuspocusProvider({ - websocketProvider: socket, name: id, parameters: realtimeConfig.queryParams, // using user id as a token to verify the user on the server diff --git a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts index 6551a333caf..fcf02cc3aa0 100644 --- a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts @@ -22,7 +22,6 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit realtimeConfig, serverHandler, user, - socket, } = props; // states const [hasServerConnectionFailed, setHasServerConnectionFailed] = useState(false); @@ -31,8 +30,8 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit const provider = useMemo( () => new HocuspocusProvider({ - websocketProvider: socket, name: id, + url: realtimeConfig.url, token: JSON.stringify(user), parameters: realtimeConfig.queryParams, onAuthenticationFailed: () => { diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 6526a1fdf40..4800fa0e1b2 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -161,11 +161,6 @@ export const PageEditorBody: React.FC = observer((props) => { if (pageId === undefined || !realtimeConfig) return ; - const socket = useMemo(() => getSocketConnection(realtimeConfig, currentUser?.id), [realtimeConfig]); - - if (!socket) return ; - // console.log("socket connection", socket); - return (
= observer((props) => {
{isContentEditable ? ( = observer((props) => { /> ) : ( Date: Mon, 18 Nov 2024 14:13:01 +0530 Subject: [PATCH 19/31] fix: fixing the cleanup deps for the provider and localprovider --- .../editors/document/collaborative-editor.tsx | 2 -- .../core/hooks/use-collaborative-editor.ts | 26 ++++++++----------- .../use-read-only-collaborative-editor.ts | 24 +++++++---------- 3 files changed, 21 insertions(+), 31 deletions(-) diff --git a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx index 49b391ac94e..cd7d6f35489 100644 --- a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx +++ b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx @@ -14,7 +14,6 @@ import { EditorRefApi, ICollaborativeDocumentEditor } from "@/types"; const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => { const { - socket, onTransaction, aiHandler, containerClassName, @@ -50,7 +49,6 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => { editorClassName, embedHandler, extensions, - socket, fileHandler, forwardedRef, handleEditorReady, diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 38af11ccfe6..665259604d7 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -53,32 +53,28 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { setHasServerConnectionFailed(true); } }, - preserveConnection: true, onSynced: () => setHasServerSynced(true), }), [id, realtimeConfig, serverHandler, user] ); + // indexed db integration for offline support + const localProvider = useMemo(() => { + if (id) { + const localProvider = new IndexeddbPersistence(id, provider.document); + return localProvider; + } + }, [id, provider]); + // destroy and disconnect connection on unmount useEffect( () => () => { - setTimeout(() => { - console.log("destroying provider", id); - provider.destroy(); - }, 4000); - // provider.destroy(); + provider?.destroy(); + localProvider?.destroy(); }, - [] + [provider] ); - // indexed db integration for offline support - useLayoutEffect(() => { - const localProvider = new IndexeddbPersistence(id, provider.document); - return () => { - localProvider?.destroy(); - }; - }, [provider, id]); - const editor = useEditor({ id, onTransaction, diff --git a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts index fcf02cc3aa0..cdc1388681f 100644 --- a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts @@ -45,32 +45,28 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit setHasServerConnectionFailed(true); } }, - preserveConnection: true, onSynced: () => setHasServerSynced(true), }), [id, realtimeConfig, serverHandler, user.id] ); + // indexed db integration for offline support + const localProvider = useMemo(() => { + if (id) { + const localProvider = new IndexeddbPersistence(id, provider.document); + return localProvider; + } + }, [id, provider]); + // destroy and disconnect connection on unmount useEffect( () => () => { - setTimeout(() => { - console.log("destroying read only provider", id); - provider.destroy(); - }, 4000); - // provider.destroy(); + provider.destroy(); + localProvider?.destroy(); }, [provider] ); - // indexed db integration for offline support - useLayoutEffect(() => { - const localProvider = new IndexeddbPersistence(id, provider.document); - return () => { - localProvider?.destroy(); - }; - }, [provider, id]); - const editor = useReadOnlyEditor({ editorProps, editorClassName, From a4e1b677060604db918463d804bf2feb3a7e3804 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 19 Nov 2024 17:43:24 +0530 Subject: [PATCH 20/31] fix: add a better data structure for managing events --- live/src/core/hocuspocus-server.ts | 4 +- .../custom-image/components/image-node.tsx | 9 +- .../src/core/helpers/document-events.ts | 27 ++- .../core/hooks/use-collaborative-editor.ts | 4 +- .../editor/src/core/types/document-events.ts | 6 +- packages/editor/src/core/types/editor.ts | 5 +- .../pages/editor/header/options-dropdown.tsx | 177 ++++++++---------- 7 files changed, 110 insertions(+), 122 deletions(-) diff --git a/live/src/core/hocuspocus-server.ts b/live/src/core/hocuspocus-server.ts index 6ba236300c5..fb0275464c2 100644 --- a/live/src/core/hocuspocus-server.ts +++ b/live/src/core/hocuspocus-server.ts @@ -6,6 +6,7 @@ import { handleAuthentication } from "@/core/lib/authentication.js"; import { getExtensions } from "@/core/extensions/index.js"; import { DocumentEventResponses, + DocumentRealtimeEvents, TDocumentEventsServer, } from "@plane/editor/lib"; // editor types @@ -60,7 +61,8 @@ export const getHocusPocusServer = async () => { } }, async onStateless({ payload, document }) { - const response = DocumentEventResponses[payload as TDocumentEventsServer]; + const response = + DocumentRealtimeEvents[payload as TDocumentEventsServer].client; if (response) { document.broadcastStateless(response); } diff --git a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx index 78caa87b301..58b60b306d6 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx @@ -29,12 +29,9 @@ export const CustomImageNode = (props: CustomImageNodeProps) => { useEffect(() => { const closestEditorContainer = imageComponentRef.current?.closest(".editor-container"); - if (!closestEditorContainer) { - console.error("Editor container not found"); - return; + if (closestEditorContainer) { + setEditorContainer(closestEditorContainer as HTMLDivElement); } - - setEditorContainer(closestEditorContainer as HTMLDivElement); }, []); // the image is already uploaded if the image-component node has src attribute @@ -55,7 +52,7 @@ export const CustomImageNode = (props: CustomImageNodeProps) => { setResolvedSrc(url as string); }; getImageSource(); - }, [imageFromFileSystem, node.attrs.src]); + }, [imgNodeSrc]); return ( diff --git a/packages/editor/src/core/helpers/document-events.ts b/packages/editor/src/core/helpers/document-events.ts index 649471f1cd5..cb8cbd72df6 100644 --- a/packages/editor/src/core/helpers/document-events.ts +++ b/packages/editor/src/core/helpers/document-events.ts @@ -1,6 +1,21 @@ -export enum DocumentEventResponses { - Lock = "locked", - Unlock = "unlocked", - Archive = "archived", - Unarchive = "unarchived", -} +import { TDocumentEventsClient, TDocumentEventsServer } from "src/lib"; + +export const DocumentRealtimeEvents = { + Lock: { client: "locked", server: "Lock" }, + Unlock: { client: "unlocked", server: "Unlock" }, + Archive: { client: "archived", server: "Archive" }, + Unarchive: { client: "unarchived", server: "Unarchive" }, + Favorite: { client: "favorited", server: "Favorite" }, + RemoveFavorite: { client: "removed-favorite", server: "RemoveFavorite" }, +} as const; + +export type DocumentEventKey = keyof typeof DocumentRealtimeEvents; + +export const getServerEventName = (clientEvent: TDocumentEventsClient): TDocumentEventsServer | undefined => { + for (const key in DocumentRealtimeEvents) { + if (DocumentRealtimeEvents[key as DocumentEventKey].client === clientEvent) { + return DocumentRealtimeEvents[key as DocumentEventKey].server; + } + } + return undefined; +}; diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 665259604d7..6182b0d3f42 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { HocuspocusProvider } from "@hocuspocus/provider"; import Collaboration from "@tiptap/extension-collaboration"; import { IndexeddbPersistence } from "y-indexeddb"; @@ -66,7 +66,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { } }, [id, provider]); - // destroy and disconnect connection on unmount + // destroy and disconnect all providers connection on unmount useEffect( () => () => { provider?.destroy(); diff --git a/packages/editor/src/core/types/document-events.ts b/packages/editor/src/core/types/document-events.ts index 8bfeaab3537..d686cff3372 100644 --- a/packages/editor/src/core/types/document-events.ts +++ b/packages/editor/src/core/types/document-events.ts @@ -1,4 +1,4 @@ -import { DocumentEventResponses } from "@/helpers/document-events"; +import { DocumentEventKey, DocumentRealtimeEvents } from "@/helpers/document-events"; -export type TDocumentEventsServer = keyof typeof DocumentEventResponses; -export type TDocumentEventsClient = `${DocumentEventResponses}`; +export type TDocumentEventsClient = (typeof DocumentRealtimeEvents)[DocumentEventKey]["client"]; +export type TDocumentEventsServer = (typeof DocumentRealtimeEvents)[DocumentEventKey]["server"]; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index 3ebf57a40f1..a76bba10f46 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -14,7 +14,8 @@ import { TServerHandler, } from "@/types"; import { TTextAlign } from "@/extensions"; -import { HocuspocusProviderWebsocket } from "@hocuspocus/provider"; +import { HocuspocusProvider, HocuspocusProviderWebsocket } from "@hocuspocus/provider"; +import { TDocumentEventsServer } from "src/lib"; export type TEditorCommands = | "text" @@ -84,6 +85,8 @@ export type EditorReadOnlyRefApi = { }; onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void; getHeadings: () => IMarking[]; + emitRealTimeUpdate: (action: TDocumentEventsServer) => void; + listenToRealTimeUpdate: () => HocuspocusProvider; }; export interface EditorRefApi extends EditorReadOnlyRefApi { diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index fc760641331..d14fe2ca4af 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useMemo } from "react"; import { observer } from "mobx-react"; import { useParams, useRouter } from "next/navigation"; import { @@ -16,9 +16,9 @@ import { } from "lucide-react"; // document editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; -import { TDocumentEventsClient } from "@plane/editor/lib"; +import { DocumentRealtimeEvents, TDocumentEventsClient, getServerEventName } from "@plane/editor/lib"; // ui -import { ArchiveIcon, CustomMenu, ISvgIcons, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; +import { ArchiveIcon, CustomMenu, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; // components import { ExportPageModal } from "@/components/pages"; // helpers @@ -35,6 +35,11 @@ type Props = { page: IPage; }; +type ActionDetails = { + action: () => Promise; + errorMessage: string; +}; + export const PageOptionsDropdown: React.FC = observer((props) => { const { editorRef, handleDuplicatePage, page } = props; // router @@ -65,95 +70,53 @@ export const PageOptionsDropdown: React.FC = observer((props) => { // update query params const { updateQueryParams } = useQueryParams(); - const handleArchivePage = useCallback( - async (isPerformedByCurrentUser: boolean = true) => { - await archive() - .then(() => { - if (isPerformedByCurrentUser) { - setCurrentUserAction("archived"); - } - }) - .catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be archived. Please try again later.", - }); - }); - }, - [archive] - ); - - const handleRestorePage = useCallback( - async (isPerformedByCurrentUser: boolean = true) => { - await restore() - .then(() => { - if (isPerformedByCurrentUser) { - setCurrentUserAction("unarchived"); - } - }) - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be restored. Please try again later.", - }) - ); - }, - [restore] - ); - - const handleLockPage = useCallback( - async (isPerformedByCurrentUser: boolean = true) => { - await lock() - .then(() => { - if (isPerformedByCurrentUser) { - setCurrentUserAction("locked"); - } - }) - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be locked. Please try again later.", - }) - ); - }, - [lock] + const EVENT_ACTION_DETAILS_MAP: Record = useMemo( + () => ({ + [DocumentRealtimeEvents.Lock.client]: { + action: lock, + errorMessage: "Page could not be locked. Please try again later.", + }, + [DocumentRealtimeEvents.Unlock.client]: { + action: unlock, + errorMessage: "Page could not be unlocked. Please try again later.", + }, + [DocumentRealtimeEvents.Archive.client]: { + action: archive, + errorMessage: "Page could not be archived. Please try again later.", + }, + [DocumentRealtimeEvents.Unarchive.client]: { + action: restore, + errorMessage: "Page could not be restored. Please try again later.", + }, + }), + [lock, unlock, archive, restore] ); - const handleUnlockPage = useCallback( - async (isPerformedByCurrentUser: boolean = true) => { - await unlock() - .then(() => { - if (isPerformedByCurrentUser) { - setCurrentUserAction("unlocked"); - } - }) - .catch(() => - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Page could not be unlocked. Please try again later.", - }) - ); + const handlePageAction = useCallback( + async (actionDetails: ActionDetails, event: TDocumentEventsClient, isPerformedByCurrentUser: boolean = true) => { + try { + await actionDetails.action(); + if (isPerformedByCurrentUser) { + setCurrentUserAction(event); + } + } catch { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: actionDetails.errorMessage, + }); + } }, - [unlock] + [] ); - // this is for the emitting real time updates for the current user's action + // sending the current user action to the server useEffect(() => { - if (currentUserAction === "archived") { - editorRef?.emitRealTimeUpdate("Archive"); - } - if (currentUserAction === "unarchived") { - editorRef?.emitRealTimeUpdate("Unarchive"); - } - if (currentUserAction === "locked") { - editorRef?.emitRealTimeUpdate("Lock"); - } - if (currentUserAction === "unlocked") { - editorRef?.emitRealTimeUpdate("Unlock"); + if (currentUserAction) { + const serverEventName = getServerEventName(currentUserAction); + if (serverEventName) { + editorRef?.emitRealTimeUpdate(serverEventName); + } } }, [currentUserAction, editorRef]); @@ -168,19 +131,9 @@ export const PageOptionsDropdown: React.FC = observer((props) => { return; } - switch (message.payload) { - case "locked": - handleLockPage(false); - break; - case "unlocked": - handleUnlockPage(false); - break; - case "archived": - handleArchivePage(false); - break; - case "unarchived": - handleRestorePage(false); - break; + const eventActions = EVENT_ACTION_DETAILS_MAP[message.payload]; + if (eventActions) { + handlePageAction(eventActions, message.payload, false); } }; @@ -189,14 +142,14 @@ export const PageOptionsDropdown: React.FC = observer((props) => { return () => { provider?.off("stateless", handleStatelessMessage); }; - }, [editorRef, currentUserAction, handleArchivePage, handleRestorePage, handleLockPage, handleUnlockPage]); + }, [editorRef, currentUserAction, handlePageAction, EVENT_ACTION_DETAILS_MAP]); // menu items list const MENU_ITEMS: { key: string; action: () => void; label: string; - icon: LucideIcon | React.FC; + icon: LucideIcon; shouldRender: boolean; }[] = [ { @@ -242,14 +195,32 @@ export const PageOptionsDropdown: React.FC = observer((props) => { }, { key: "lock-unlock-page", - action: is_locked ? handleUnlockPage : handleLockPage, + // action: is_locked ? handleUnlockPage : handleLockPage, + action: is_locked + ? () => { + const clientAction = DocumentRealtimeEvents["Unlock"].client; + handlePageAction(EVENT_ACTION_DETAILS_MAP[clientAction], clientAction, true); + } + : () => { + const clientAction = DocumentRealtimeEvents["Lock"].client; + handlePageAction(EVENT_ACTION_DETAILS_MAP[clientAction], clientAction, true); + }, label: is_locked ? "Unlock page" : "Lock page", icon: is_locked ? LockOpen : Lock, shouldRender: canCurrentUserLockPage, }, { key: "archive-restore-page", - action: archived_at ? handleRestorePage : handleArchivePage, + // action: archived_at ? handleRestorePage : handleArchivePage, + action: archived_at + ? () => { + const clientAction = DocumentRealtimeEvents["Unarchive"].client; + handlePageAction(EVENT_ACTION_DETAILS_MAP[clientAction], clientAction, true); + } + : () => { + const clientAction = DocumentRealtimeEvents["Archive"].client; + handlePageAction(EVENT_ACTION_DETAILS_MAP[clientAction], clientAction, true); + }, label: archived_at ? "Restore page" : "Archive page", icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon, shouldRender: canCurrentUserArchivePage, From bb715d2e89a4da3510cfa731b30b1d13a1dd9a54 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Fri, 22 Nov 2024 18:11:14 +0530 Subject: [PATCH 21/31] chore: refactored realtime events into hooks --- .../components/pages/editor/editor-body.tsx | 2 +- .../pages/editor/header/options-dropdown.tsx | 103 ++---------------- web/core/hooks/use-live-server-realtime.tsx | 100 +++++++++++++++++ 3 files changed, 108 insertions(+), 97 deletions(-) create mode 100644 web/core/hooks/use-live-server-realtime.tsx diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 4800fa0e1b2..034659fa02b 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -180,7 +180,7 @@ export const PageEditorBody: React.FC = observer((props) => { "md:w-[90%]": isFullWidth, })} > -
+
= observer((props) => { archived_at, is_locked, id, - archive, - lock, - unlock, canCurrentUserArchivePage, canCurrentUserDuplicatePage, canCurrentUserLockPage, - restore, } = page; // states const [isExportModalOpen, setIsExportModalOpen] = useState(false); @@ -69,80 +66,8 @@ export const PageOptionsDropdown: React.FC = observer((props) => { const { isFullWidth, handleFullWidth } = usePageFilters(); // update query params const { updateQueryParams } = useQueryParams(); - - const EVENT_ACTION_DETAILS_MAP: Record = useMemo( - () => ({ - [DocumentRealtimeEvents.Lock.client]: { - action: lock, - errorMessage: "Page could not be locked. Please try again later.", - }, - [DocumentRealtimeEvents.Unlock.client]: { - action: unlock, - errorMessage: "Page could not be unlocked. Please try again later.", - }, - [DocumentRealtimeEvents.Archive.client]: { - action: archive, - errorMessage: "Page could not be archived. Please try again later.", - }, - [DocumentRealtimeEvents.Unarchive.client]: { - action: restore, - errorMessage: "Page could not be restored. Please try again later.", - }, - }), - [lock, unlock, archive, restore] - ); - - const handlePageAction = useCallback( - async (actionDetails: ActionDetails, event: TDocumentEventsClient, isPerformedByCurrentUser: boolean = true) => { - try { - await actionDetails.action(); - if (isPerformedByCurrentUser) { - setCurrentUserAction(event); - } - } catch { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: actionDetails.errorMessage, - }); - } - }, - [] - ); - - // sending the current user action to the server - useEffect(() => { - if (currentUserAction) { - const serverEventName = getServerEventName(currentUserAction); - if (serverEventName) { - editorRef?.emitRealTimeUpdate(serverEventName); - } - } - }, [currentUserAction, editorRef]); - - // this is for listening to real time updates from the live server for remote - // users' actions - useEffect(() => { - const provider = editorRef?.listenToRealTimeUpdate(); - - const handleStatelessMessage = (message: { payload: TDocumentEventsClient }) => { - if (currentUserAction === message.payload) { - setCurrentUserAction(null); - return; - } - - const eventActions = EVENT_ACTION_DETAILS_MAP[message.payload]; - if (eventActions) { - handlePageAction(eventActions, message.payload, false); - } - }; - - provider?.on("stateless", handleStatelessMessage); - - return () => { - provider?.off("stateless", handleStatelessMessage); - }; - }, [editorRef, currentUserAction, handlePageAction, EVENT_ACTION_DETAILS_MAP]); + // collaborative actions + const { executeCollaborativeAction } = usePageCollaborativeActions(editorRef, page); // menu items list const MENU_ITEMS: { @@ -195,32 +120,18 @@ export const PageOptionsDropdown: React.FC = observer((props) => { }, { key: "lock-unlock-page", - // action: is_locked ? handleUnlockPage : handleLockPage, action: is_locked - ? () => { - const clientAction = DocumentRealtimeEvents["Unlock"].client; - handlePageAction(EVENT_ACTION_DETAILS_MAP[clientAction], clientAction, true); - } - : () => { - const clientAction = DocumentRealtimeEvents["Lock"].client; - handlePageAction(EVENT_ACTION_DETAILS_MAP[clientAction], clientAction, true); - }, + ? () => executeCollaborativeAction({ type: "sendToServer", message: "Unlock" }) + : () => executeCollaborativeAction({ type: "sendToServer", message: "Lock" }), label: is_locked ? "Unlock page" : "Lock page", icon: is_locked ? LockOpen : Lock, shouldRender: canCurrentUserLockPage, }, { key: "archive-restore-page", - // action: archived_at ? handleRestorePage : handleArchivePage, action: archived_at - ? () => { - const clientAction = DocumentRealtimeEvents["Unarchive"].client; - handlePageAction(EVENT_ACTION_DETAILS_MAP[clientAction], clientAction, true); - } - : () => { - const clientAction = DocumentRealtimeEvents["Archive"].client; - handlePageAction(EVENT_ACTION_DETAILS_MAP[clientAction], clientAction, true); - }, + ? () => executeCollaborativeAction({ type: "sendToServer", message: "Unarchive" }) + : () => executeCollaborativeAction({ type: "sendToServer", message: "Archive" }), label: archived_at ? "Restore page" : "Archive page", icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon, shouldRender: canCurrentUserArchivePage, diff --git a/web/core/hooks/use-live-server-realtime.tsx b/web/core/hooks/use-live-server-realtime.tsx new file mode 100644 index 00000000000..6ffdd066ce3 --- /dev/null +++ b/web/core/hooks/use-live-server-realtime.tsx @@ -0,0 +1,100 @@ +import { useState, useEffect, useCallback, useMemo } from "react"; +import { EditorReadOnlyRefApi, EditorRefApi, TDocumentEventsServer } from "@plane/editor"; +import { DocumentRealtimeEvents, TDocumentEventsClient, getServerEventName } from "@plane/editor/lib"; +import { TOAST_TYPE, setToast } from "@plane/ui"; +import { IPage } from "@/store/pages/page"; + +// Better type naming and structure +type CollaborativeAction = { + execute: () => Promise; + errorMessage: string; +}; + +type CollaborativeActionEvent = + | { type: "sendToServer"; message: TDocumentEventsServer } + | { type: "fromServer"; message: TDocumentEventsClient }; + +export const usePageCollaborativeActions = (editorRef: EditorRefApi | EditorReadOnlyRefApi | null, page: IPage) => { + // currentUserAction local state to track if the current action is being processed, a + // local action is basically the action performed by the current user to avoid double operations + const [currentActionBeingProcessed, setCurrentActionBeingProcessed] = useState(null); + + const actionHandlerMap: Record = useMemo( + () => ({ + [DocumentRealtimeEvents.Lock.client]: { + execute: page.lock, + errorMessage: "Page could not be locked. Please try again later.", + }, + [DocumentRealtimeEvents.Unlock.client]: { + execute: page.unlock, + errorMessage: "Page could not be unlocked. Please try again later.", + }, + [DocumentRealtimeEvents.Archive.client]: { + execute: page.archive, + errorMessage: "Page could not be archived. Please try again later.", + }, + [DocumentRealtimeEvents.Unarchive.client]: { + execute: page.restore, + errorMessage: "Page could not be restored. Please try again later.", + }, + }), + [page.lock, page.unlock, page.archive, page.restore] + ); + + const executeCollaborativeAction = useCallback( + async (event: CollaborativeActionEvent) => { + const isPerformedByCurrentUser = event.type === "sendToServer"; + const clientAction = isPerformedByCurrentUser ? DocumentRealtimeEvents[event.message].client : event.message; + const actionDetails = actionHandlerMap[clientAction]; + + try { + await actionDetails.execute(); + if (isPerformedByCurrentUser) { + setCurrentActionBeingProcessed(clientAction); + } + } catch { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: actionDetails.errorMessage, + }); + } + }, + [actionHandlerMap] + ); + + useEffect(() => { + if (currentActionBeingProcessed) { + const serverEventName = getServerEventName(currentActionBeingProcessed); + if (serverEventName) { + editorRef?.emitRealTimeUpdate(serverEventName); + } + } + }, [currentActionBeingProcessed, editorRef]); + + useEffect(() => { + const provider = editorRef?.listenToRealTimeUpdate(); + + const handleStatelessMessage = (message: { payload: TDocumentEventsClient }) => { + if (currentActionBeingProcessed === message.payload) { + setCurrentActionBeingProcessed(null); + return; + } + + if (message.payload) { + executeCollaborativeAction({ type: "fromServer", message: message.payload }); + } + }; + + provider?.on("stateless", handleStatelessMessage); + + return () => { + provider?.off("stateless", handleStatelessMessage); + }; + }, [editorRef, currentActionBeingProcessed, executeCollaborativeAction, actionHandlerMap]); + + return { + executeCollaborativeAction, + EVENT_ACTION_DETAILS_MAP: actionHandlerMap, + }; +}; From 5362a2c2991b5beb7fcb38965c7f285fefecd2a3 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Fri, 22 Nov 2024 18:12:57 +0530 Subject: [PATCH 22/31] feat: fetch page meta while focusing tabs --- .../[projectId]/pages/(detail)/[pageId]/page.tsx | 6 +++--- .../pages/editor/header/options-dropdown.tsx | 11 +---------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx index 2d9182d3831..ebbf4d68516 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx @@ -32,9 +32,9 @@ const PageDetailsPage = observer(() => { ? () => getPageById(workspaceSlug?.toString(), projectId?.toString(), pageId.toString()) : null, { - revalidateIfStale: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, + revalidateIfStale: true, + revalidateOnFocus: true, + revalidateOnReconnect: true, } ); diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index fc3d13574e2..bcfc6978c16 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback, useMemo } from "react"; +import { useState } from "react"; import { observer } from "mobx-react"; import { useParams, useRouter } from "next/navigation"; import { @@ -16,7 +16,6 @@ import { } from "lucide-react"; // document editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; -import { DocumentRealtimeEvents, TDocumentEventsClient, getServerEventName } from "@plane/editor/lib"; // ui import { ArchiveIcon, CustomMenu, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; // components @@ -36,11 +35,6 @@ type Props = { page: IPage; }; -type ActionDetails = { - action: () => Promise; - errorMessage: string; -}; - export const PageOptionsDropdown: React.FC = observer((props) => { const { editorRef, handleDuplicatePage, page } = props; // router @@ -57,9 +51,6 @@ export const PageOptionsDropdown: React.FC = observer((props) => { } = page; // states const [isExportModalOpen, setIsExportModalOpen] = useState(false); - // currentUserAction local state to track if the current action is being processed, a - // local action is basically the action performed by the current user to avoid double operations - const [currentUserAction, setCurrentUserAction] = useState(null); // store hooks const { workspaceSlug, projectId } = useParams(); // page filters From e8d45006dfcf7d6f6507f88571d3bd7db5de4306 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 26 Nov 2024 13:35:58 +0530 Subject: [PATCH 23/31] fix: cycling through items on slash command item in down arrow --- .../editor/src/core/extensions/slash-commands/command-menu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx index d6148b69aef..93b0ce2ea82 100644 --- a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx +++ b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx @@ -41,7 +41,7 @@ export const SlashCommandsMenu = (props: SlashCommandsMenuProps) => { if (nextItem < 0) { nextSection = currentSection - 1; if (nextSection < 0) nextSection = sections.length - 1; - nextItem = sections[nextSection].items.length - 1; + nextItem = sections[nextSection]?.items.length - 1; } } if (e.key === "ArrowDown") { From 7bc9ff2e51621779c5818530b70be3a70f9d0b59 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 26 Nov 2024 13:52:42 +0530 Subject: [PATCH 24/31] fix: better naming convention for realtime events --- live/src/core/hocuspocus-server.ts | 2 +- .../pages/editor/header/options-dropdown.tsx | 12 ++++++------ web/core/hooks/use-live-server-realtime.tsx | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/live/src/core/hocuspocus-server.ts b/live/src/core/hocuspocus-server.ts index fb0275464c2..edd35488b22 100644 --- a/live/src/core/hocuspocus-server.ts +++ b/live/src/core/hocuspocus-server.ts @@ -5,7 +5,6 @@ import { handleAuthentication } from "@/core/lib/authentication.js"; // extensions import { getExtensions } from "@/core/extensions/index.js"; import { - DocumentEventResponses, DocumentRealtimeEvents, TDocumentEventsServer, } from "@plane/editor/lib"; @@ -61,6 +60,7 @@ export const getHocusPocusServer = async () => { } }, async onStateless({ payload, document }) { + // broadcast the client event (derived from the server event) to all the clients so that they can update their state const response = DocumentRealtimeEvents[payload as TDocumentEventsServer].client; if (response) { diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index bcfc6978c16..672ec9b8b45 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -23,7 +23,7 @@ import { ExportPageModal } from "@/components/pages"; // helpers import { copyTextToClipboard, copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { usePageCollaborativeActions } from "@/hooks/use-live-server-realtime"; +import { useCollaborativePageActions } from "@/hooks/use-live-server-realtime"; import { usePageFilters } from "@/hooks/use-page-filters"; import { useQueryParams } from "@/hooks/use-query-params"; // store @@ -58,7 +58,7 @@ export const PageOptionsDropdown: React.FC = observer((props) => { // update query params const { updateQueryParams } = useQueryParams(); // collaborative actions - const { executeCollaborativeAction } = usePageCollaborativeActions(editorRef, page); + const { executeCollaborativeAction } = useCollaborativePageActions(editorRef, page); // menu items list const MENU_ITEMS: { @@ -112,8 +112,8 @@ export const PageOptionsDropdown: React.FC = observer((props) => { { key: "lock-unlock-page", action: is_locked - ? () => executeCollaborativeAction({ type: "sendToServer", message: "Unlock" }) - : () => executeCollaborativeAction({ type: "sendToServer", message: "Lock" }), + ? () => executeCollaborativeAction({ type: "sendMessageToServer", message: "Unlock" }) + : () => executeCollaborativeAction({ type: "sendMessageToServer", message: "Lock" }), label: is_locked ? "Unlock page" : "Lock page", icon: is_locked ? LockOpen : Lock, shouldRender: canCurrentUserLockPage, @@ -121,8 +121,8 @@ export const PageOptionsDropdown: React.FC = observer((props) => { { key: "archive-restore-page", action: archived_at - ? () => executeCollaborativeAction({ type: "sendToServer", message: "Unarchive" }) - : () => executeCollaborativeAction({ type: "sendToServer", message: "Archive" }), + ? () => executeCollaborativeAction({ type: "sendMessageToServer", message: "Unarchive" }) + : () => executeCollaborativeAction({ type: "sendMessageToServer", message: "Archive" }), label: archived_at ? "Restore page" : "Archive page", icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon, shouldRender: canCurrentUserArchivePage, diff --git a/web/core/hooks/use-live-server-realtime.tsx b/web/core/hooks/use-live-server-realtime.tsx index 6ffdd066ce3..bfe036ea1f3 100644 --- a/web/core/hooks/use-live-server-realtime.tsx +++ b/web/core/hooks/use-live-server-realtime.tsx @@ -11,10 +11,10 @@ type CollaborativeAction = { }; type CollaborativeActionEvent = - | { type: "sendToServer"; message: TDocumentEventsServer } - | { type: "fromServer"; message: TDocumentEventsClient }; + | { type: "sendMessageToServer"; message: TDocumentEventsServer } + | { type: "receivedMessageFromServer"; message: TDocumentEventsClient }; -export const usePageCollaborativeActions = (editorRef: EditorRefApi | EditorReadOnlyRefApi | null, page: IPage) => { +export const useCollaborativePageActions = (editorRef: EditorRefApi | EditorReadOnlyRefApi | null, page: IPage) => { // currentUserAction local state to track if the current action is being processed, a // local action is basically the action performed by the current user to avoid double operations const [currentActionBeingProcessed, setCurrentActionBeingProcessed] = useState(null); @@ -43,7 +43,7 @@ export const usePageCollaborativeActions = (editorRef: EditorRefApi | EditorRead const executeCollaborativeAction = useCallback( async (event: CollaborativeActionEvent) => { - const isPerformedByCurrentUser = event.type === "sendToServer"; + const isPerformedByCurrentUser = event.type === "sendMessageToServer"; const clientAction = isPerformedByCurrentUser ? DocumentRealtimeEvents[event.message].client : event.message; const actionDetails = actionHandlerMap[clientAction]; @@ -82,7 +82,7 @@ export const usePageCollaborativeActions = (editorRef: EditorRefApi | EditorRead } if (message.payload) { - executeCollaborativeAction({ type: "fromServer", message: message.payload }); + executeCollaborativeAction({ type: "receivedMessageFromServer", message: message.payload }); } }; From 0626f164b0a8fa09fe3838061fb647701776dba2 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 26 Nov 2024 14:07:47 +0530 Subject: [PATCH 25/31] fix: simplified localprovider initialization and cleaning --- .../editor/src/core/hooks/use-collaborative-editor.ts | 11 ++++------- .../core/hooks/use-read-only-collaborative-editor.ts | 10 ++++------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 6182b0d3f42..faed428916d 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -58,13 +58,10 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { [id, realtimeConfig, serverHandler, user] ); - // indexed db integration for offline support - const localProvider = useMemo(() => { - if (id) { - const localProvider = new IndexeddbPersistence(id, provider.document); - return localProvider; - } - }, [id, provider]); + const localProvider = useMemo( + () => (id ? new IndexeddbPersistence(id, provider.document) : undefined), + [id, provider] + ); // destroy and disconnect all providers connection on unmount useEffect( diff --git a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts index cdc1388681f..30cfd051381 100644 --- a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts @@ -51,12 +51,10 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit ); // indexed db integration for offline support - const localProvider = useMemo(() => { - if (id) { - const localProvider = new IndexeddbPersistence(id, provider.document); - return localProvider; - } - }, [id, provider]); + const localProvider = useMemo( + () => (id ? new IndexeddbPersistence(id, provider.document) : undefined), + [id, provider] + ); // destroy and disconnect connection on unmount useEffect( From 2d648c74c0fc34a23d173c8198f426f01f4fd7f1 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 26 Nov 2024 14:13:05 +0530 Subject: [PATCH 26/31] fix: types from ui --- web/core/components/pages/editor/header/options-dropdown.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 672ec9b8b45..13b81fa4155 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -17,7 +17,7 @@ import { // document editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; // ui -import { ArchiveIcon, CustomMenu, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; +import { ArchiveIcon, CustomMenu, type ISvgIcons, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; // components import { ExportPageModal } from "@/components/pages"; // helpers @@ -65,7 +65,7 @@ export const PageOptionsDropdown: React.FC = observer((props) => { key: string; action: () => void; label: string; - icon: LucideIcon; + icon: LucideIcon | React.FC; shouldRender: boolean; }[] = [ { From 6c9bcf13af53b9209289ba49b252a97b43430e5a Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 26 Nov 2024 14:42:22 +0530 Subject: [PATCH 27/31] fix: abstracted away from exposing the provider directly --- packages/editor/src/core/hooks/use-editor.ts | 3 ++- packages/editor/src/core/hooks/use-read-only-editor.ts | 2 +- packages/editor/src/core/types/document-events.ts | 5 +++++ packages/editor/src/core/types/editor.ts | 6 +++--- web/core/components/pages/editor/editor-body.tsx | 1 - .../components/pages/editor/header/options-dropdown.tsx | 2 +- ...rver-realtime.tsx => use-collaborative-page-actions.tsx} | 6 +++--- 7 files changed, 15 insertions(+), 10 deletions(-) rename web/core/hooks/{use-live-server-realtime.tsx => use-collaborative-page-actions.tsx} (93%) diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index bef6ced428d..77733d212a0 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -23,6 +23,7 @@ import type { IMentionSuggestion, TEditorCommands, TFileHandler, + TDocumentEventEmitter, } from "@/types"; export interface CustomEditorProps { @@ -296,7 +297,7 @@ export const useEditor = (props: CustomEditorProps) => { Y.applyUpdate(document, value); }, emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message), - listenToRealTimeUpdate: () => provider, + listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) }, }), [editorRef, savedSelection] ); diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index e79edc3a2c3..b6b1a8a15a6 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -118,7 +118,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { }; }, emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message), - listenToRealTimeUpdate: () => provider, + listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) }, getHeadings: () => editorRef?.current?.storage.headingList.headings, })); diff --git a/packages/editor/src/core/types/document-events.ts b/packages/editor/src/core/types/document-events.ts index d686cff3372..f05966713a9 100644 --- a/packages/editor/src/core/types/document-events.ts +++ b/packages/editor/src/core/types/document-events.ts @@ -2,3 +2,8 @@ import { DocumentEventKey, DocumentRealtimeEvents } from "@/helpers/document-eve export type TDocumentEventsClient = (typeof DocumentRealtimeEvents)[DocumentEventKey]["client"]; export type TDocumentEventsServer = (typeof DocumentRealtimeEvents)[DocumentEventKey]["server"]; + +export type TDocumentEventEmitter = { + on: (event: string, callback: (message: { payload: TDocumentEventsClient }) => void) => void; + off: (event: string, callback: (message: { payload: TDocumentEventsClient }) => void) => void; +}; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index a76bba10f46..ad1ddd24041 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -14,8 +14,8 @@ import { TServerHandler, } from "@/types"; import { TTextAlign } from "@/extensions"; -import { HocuspocusProvider, HocuspocusProviderWebsocket } from "@hocuspocus/provider"; -import { TDocumentEventsServer } from "src/lib"; +import { HocuspocusProviderWebsocket } from "@hocuspocus/provider"; +import { TDocumentEventsServer, TDocumentEventEmitter } from "src/lib"; export type TEditorCommands = | "text" @@ -86,7 +86,7 @@ export type EditorReadOnlyRefApi = { onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void; getHeadings: () => IMarking[]; emitRealTimeUpdate: (action: TDocumentEventsServer) => void; - listenToRealTimeUpdate: () => HocuspocusProvider; + listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined; }; export interface EditorRefApi extends EditorReadOnlyRefApi { diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index 034659fa02b..5b68bfb8818 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -35,7 +35,6 @@ import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed"; import { FileService } from "@/services/file.service"; // store import { IPage } from "@/store/pages/page"; -import { getSocketConnection } from "./socket"; // services init const fileService = new FileService(); diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index 13b81fa4155..e2eda4c6491 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -23,7 +23,7 @@ import { ExportPageModal } from "@/components/pages"; // helpers import { copyTextToClipboard, copyUrlToClipboard } from "@/helpers/string.helper"; // hooks -import { useCollaborativePageActions } from "@/hooks/use-live-server-realtime"; +import { useCollaborativePageActions } from "@/hooks/use-collaborative-page-actions"; import { usePageFilters } from "@/hooks/use-page-filters"; import { useQueryParams } from "@/hooks/use-query-params"; // store diff --git a/web/core/hooks/use-live-server-realtime.tsx b/web/core/hooks/use-collaborative-page-actions.tsx similarity index 93% rename from web/core/hooks/use-live-server-realtime.tsx rename to web/core/hooks/use-collaborative-page-actions.tsx index bfe036ea1f3..09b3621e317 100644 --- a/web/core/hooks/use-live-server-realtime.tsx +++ b/web/core/hooks/use-collaborative-page-actions.tsx @@ -73,7 +73,7 @@ export const useCollaborativePageActions = (editorRef: EditorRefApi | EditorRead }, [currentActionBeingProcessed, editorRef]); useEffect(() => { - const provider = editorRef?.listenToRealTimeUpdate(); + const realTimeStatelessMessageListener = editorRef?.listenToRealTimeUpdate(); const handleStatelessMessage = (message: { payload: TDocumentEventsClient }) => { if (currentActionBeingProcessed === message.payload) { @@ -86,10 +86,10 @@ export const useCollaborativePageActions = (editorRef: EditorRefApi | EditorRead } }; - provider?.on("stateless", handleStatelessMessage); + realTimeStatelessMessageListener?.on("stateless", handleStatelessMessage); return () => { - provider?.off("stateless", handleStatelessMessage); + realTimeStatelessMessageListener?.off("stateless", handleStatelessMessage); }; }, [editorRef, currentActionBeingProcessed, executeCollaborativeAction, actionHandlerMap]); From 173e93fa02289b67ce6de9427fa6efddf1814e16 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 26 Nov 2024 19:13:47 +0530 Subject: [PATCH 28/31] fix: coderabbit suggestions --- live/src/core/hocuspocus-server.ts | 4 ++-- .../document-collaborative-events.ts | 6 ++++++ .../src/core/helpers/document-events.ts | 19 ------------------- .../core/helpers/get-document-server-event.ts | 11 +++++++++++ .../core/hooks/use-collaborative-editor.ts | 2 +- .../use-read-only-collaborative-editor.ts | 2 +- .../types/document-collaborative-events.ts | 10 ++++++++++ .../editor/src/core/types/document-events.ts | 9 --------- packages/editor/src/core/types/editor.ts | 6 ++---- packages/editor/src/core/types/index.ts | 2 +- packages/editor/src/lib.ts | 5 +++-- packages/ui/src/icons/{type.d.ts => type.ts} | 0 .../pages/(detail)/[pageId]/page.tsx | 1 - .../components/pages/editor/editor-body.tsx | 2 +- .../hooks/use-collaborative-page-actions.tsx | 14 +++++++------- 15 files changed, 45 insertions(+), 48 deletions(-) create mode 100644 packages/editor/src/core/constants/document-collaborative-events.ts delete mode 100644 packages/editor/src/core/helpers/document-events.ts create mode 100644 packages/editor/src/core/helpers/get-document-server-event.ts create mode 100644 packages/editor/src/core/types/document-collaborative-events.ts delete mode 100644 packages/editor/src/core/types/document-events.ts rename packages/ui/src/icons/{type.d.ts => type.ts} (100%) diff --git a/live/src/core/hocuspocus-server.ts b/live/src/core/hocuspocus-server.ts index edd35488b22..51896c23bce 100644 --- a/live/src/core/hocuspocus-server.ts +++ b/live/src/core/hocuspocus-server.ts @@ -5,7 +5,7 @@ import { handleAuthentication } from "@/core/lib/authentication.js"; // extensions import { getExtensions } from "@/core/extensions/index.js"; import { - DocumentRealtimeEvents, + DocumentCollaborativeEvents, TDocumentEventsServer, } from "@plane/editor/lib"; // editor types @@ -62,7 +62,7 @@ export const getHocusPocusServer = async () => { async onStateless({ payload, document }) { // broadcast the client event (derived from the server event) to all the clients so that they can update their state const response = - DocumentRealtimeEvents[payload as TDocumentEventsServer].client; + DocumentCollaborativeEvents[payload as TDocumentEventsServer].client; if (response) { document.broadcastStateless(response); } diff --git a/packages/editor/src/core/constants/document-collaborative-events.ts b/packages/editor/src/core/constants/document-collaborative-events.ts new file mode 100644 index 00000000000..ae001e383ea --- /dev/null +++ b/packages/editor/src/core/constants/document-collaborative-events.ts @@ -0,0 +1,6 @@ +export const DocumentCollaborativeEvents = { + Lock: { client: "locked", server: "Lock" }, + Unlock: { client: "unlocked", server: "Unlock" }, + Archive: { client: "archived", server: "Archive" }, + Unarchive: { client: "unarchived", server: "Unarchive" }, +} as const; diff --git a/packages/editor/src/core/helpers/document-events.ts b/packages/editor/src/core/helpers/document-events.ts deleted file mode 100644 index 28c004f6c25..00000000000 --- a/packages/editor/src/core/helpers/document-events.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { TDocumentEventsClient, TDocumentEventsServer } from "src/lib"; - -export const DocumentRealtimeEvents = { - Lock: { client: "locked", server: "Lock" }, - Unlock: { client: "unlocked", server: "Unlock" }, - Archive: { client: "archived", server: "Archive" }, - Unarchive: { client: "unarchived", server: "Unarchive" }, -} as const; - -export type DocumentEventKey = keyof typeof DocumentRealtimeEvents; - -export const getServerEventName = (clientEvent: TDocumentEventsClient): TDocumentEventsServer | undefined => { - for (const key in DocumentRealtimeEvents) { - if (DocumentRealtimeEvents[key as DocumentEventKey].client === clientEvent) { - return DocumentRealtimeEvents[key as DocumentEventKey].server; - } - } - return undefined; -}; diff --git a/packages/editor/src/core/helpers/get-document-server-event.ts b/packages/editor/src/core/helpers/get-document-server-event.ts new file mode 100644 index 00000000000..1ba7646b291 --- /dev/null +++ b/packages/editor/src/core/helpers/get-document-server-event.ts @@ -0,0 +1,11 @@ +import { DocumentCollaborativeEvents } from "@/constants/document-collaborative-events"; +import { TDocumentEventKey, TDocumentEventsClient, TDocumentEventsServer } from "@/types/document-collaborative-events"; + +export const getServerEventName = (clientEvent: TDocumentEventsClient): TDocumentEventsServer | undefined => { + for (const key in DocumentCollaborativeEvents) { + if (DocumentCollaborativeEvents[key as TDocumentEventKey].client === clientEvent) { + return DocumentCollaborativeEvents[key as TDocumentEventKey].server; + } + } + return undefined; +}; diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index faed428916d..4def7e5bb95 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -69,7 +69,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { provider?.destroy(); localProvider?.destroy(); }, - [provider] + [provider, localProvider] ); const editor = useEditor({ diff --git a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts index 30cfd051381..26608aefcc9 100644 --- a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts @@ -62,7 +62,7 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit provider.destroy(); localProvider?.destroy(); }, - [provider] + [provider, localProvider] ); const editor = useReadOnlyEditor({ diff --git a/packages/editor/src/core/types/document-collaborative-events.ts b/packages/editor/src/core/types/document-collaborative-events.ts new file mode 100644 index 00000000000..99936a5ad73 --- /dev/null +++ b/packages/editor/src/core/types/document-collaborative-events.ts @@ -0,0 +1,10 @@ +import { DocumentCollaborativeEvents } from "@/constants/document-collaborative-events"; + +export type TDocumentEventKey = keyof typeof DocumentCollaborativeEvents; +export type TDocumentEventsClient = (typeof DocumentCollaborativeEvents)[TDocumentEventKey]["client"]; +export type TDocumentEventsServer = (typeof DocumentCollaborativeEvents)[TDocumentEventKey]["server"]; + +export type TDocumentEventEmitter = { + on: (event: string, callback: (message: { payload: TDocumentEventsClient }) => void) => void; + off: (event: string, callback: (message: { payload: TDocumentEventsClient }) => void) => void; +}; diff --git a/packages/editor/src/core/types/document-events.ts b/packages/editor/src/core/types/document-events.ts deleted file mode 100644 index f05966713a9..00000000000 --- a/packages/editor/src/core/types/document-events.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DocumentEventKey, DocumentRealtimeEvents } from "@/helpers/document-events"; - -export type TDocumentEventsClient = (typeof DocumentRealtimeEvents)[DocumentEventKey]["client"]; -export type TDocumentEventsServer = (typeof DocumentRealtimeEvents)[DocumentEventKey]["server"]; - -export type TDocumentEventEmitter = { - on: (event: string, callback: (message: { payload: TDocumentEventsClient }) => void) => void; - off: (event: string, callback: (message: { payload: TDocumentEventsClient }) => void) => void; -}; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index ad1ddd24041..682e9fbe4bf 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -8,14 +8,14 @@ import { IMentionSuggestion, TAIHandler, TDisplayConfig, + TDocumentEventEmitter, + TDocumentEventsServer, TEmbedConfig, TExtensions, TFileHandler, TServerHandler, } from "@/types"; import { TTextAlign } from "@/extensions"; -import { HocuspocusProviderWebsocket } from "@hocuspocus/provider"; -import { TDocumentEventsServer, TDocumentEventEmitter } from "src/lib"; export type TEditorCommands = | "text" @@ -145,7 +145,6 @@ export interface ICollaborativeDocumentEditor realtimeConfig: TRealtimeConfig; serverHandler?: TServerHandler; user: TUserDetails; - socket: HocuspocusProviderWebsocket; } // read only editor props @@ -173,7 +172,6 @@ export interface ICollaborativeDocumentReadOnlyEditor extends Omit = observer((props) => { "md:w-[90%]": isFullWidth, })} > -
+
= useMemo( () => ({ - [DocumentRealtimeEvents.Lock.client]: { + [DocumentCollaborativeEvents.Lock.client]: { execute: page.lock, errorMessage: "Page could not be locked. Please try again later.", }, - [DocumentRealtimeEvents.Unlock.client]: { + [DocumentCollaborativeEvents.Unlock.client]: { execute: page.unlock, errorMessage: "Page could not be unlocked. Please try again later.", }, - [DocumentRealtimeEvents.Archive.client]: { + [DocumentCollaborativeEvents.Archive.client]: { execute: page.archive, errorMessage: "Page could not be archived. Please try again later.", }, - [DocumentRealtimeEvents.Unarchive.client]: { + [DocumentCollaborativeEvents.Unarchive.client]: { execute: page.restore, errorMessage: "Page could not be restored. Please try again later.", }, @@ -44,7 +44,7 @@ export const useCollaborativePageActions = (editorRef: EditorRefApi | EditorRead const executeCollaborativeAction = useCallback( async (event: CollaborativeActionEvent) => { const isPerformedByCurrentUser = event.type === "sendMessageToServer"; - const clientAction = isPerformedByCurrentUser ? DocumentRealtimeEvents[event.message].client : event.message; + const clientAction = isPerformedByCurrentUser ? DocumentCollaborativeEvents[event.message].client : event.message; const actionDetails = actionHandlerMap[clientAction]; try { @@ -91,7 +91,7 @@ export const useCollaborativePageActions = (editorRef: EditorRefApi | EditorRead return () => { realTimeStatelessMessageListener?.off("stateless", handleStatelessMessage); }; - }, [editorRef, currentActionBeingProcessed, executeCollaborativeAction, actionHandlerMap]); + }, [editorRef, currentActionBeingProcessed, executeCollaborativeAction]); return { executeCollaborativeAction, From 1a66f3c5ca703e2882f7845959e923fe8ecdd510 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 26 Nov 2024 19:35:02 +0530 Subject: [PATCH 29/31] regression: pass user in dependency array --- .../src/core/hooks/use-read-only-collaborative-editor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts index 26608aefcc9..927e85caa59 100644 --- a/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-collaborative-editor.ts @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { HocuspocusProvider } from "@hocuspocus/provider"; import Collaboration from "@tiptap/extension-collaboration"; import { IndexeddbPersistence } from "y-indexeddb"; @@ -47,7 +47,7 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit }, onSynced: () => setHasServerSynced(true), }), - [id, realtimeConfig, serverHandler, user.id] + [id, realtimeConfig, serverHandler, user] ); // indexed db integration for offline support From 429bbc406c3d5496f583572f91957340eadc4ace Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Tue, 26 Nov 2024 21:05:34 +0530 Subject: [PATCH 30/31] fix: removed page action api calls by the other users the document is synced with --- .../document-collaborative-events.ts | 8 +- .../pages/editor/header/options-dropdown.tsx | 8 +- .../hooks/use-collaborative-page-actions.tsx | 22 ++--- web/core/store/pages/page.ts | 86 +++++++++++++------ 4 files changed, 78 insertions(+), 46 deletions(-) diff --git a/packages/editor/src/core/constants/document-collaborative-events.ts b/packages/editor/src/core/constants/document-collaborative-events.ts index ae001e383ea..5e79efc7a71 100644 --- a/packages/editor/src/core/constants/document-collaborative-events.ts +++ b/packages/editor/src/core/constants/document-collaborative-events.ts @@ -1,6 +1,6 @@ export const DocumentCollaborativeEvents = { - Lock: { client: "locked", server: "Lock" }, - Unlock: { client: "unlocked", server: "Unlock" }, - Archive: { client: "archived", server: "Archive" }, - Unarchive: { client: "unarchived", server: "Unarchive" }, + lock: { client: "locked", server: "lock" }, + unlock: { client: "unlocked", server: "unlock" }, + archive: { client: "archived", server: "archive" }, + unarchive: { client: "unarchived", server: "unarchive" }, } as const; diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index e2eda4c6491..ff0987a9dc2 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -112,8 +112,8 @@ export const PageOptionsDropdown: React.FC = observer((props) => { { key: "lock-unlock-page", action: is_locked - ? () => executeCollaborativeAction({ type: "sendMessageToServer", message: "Unlock" }) - : () => executeCollaborativeAction({ type: "sendMessageToServer", message: "Lock" }), + ? () => executeCollaborativeAction({ type: "sendMessageToServer", message: "unlock" }) + : () => executeCollaborativeAction({ type: "sendMessageToServer", message: "lock" }), label: is_locked ? "Unlock page" : "Lock page", icon: is_locked ? LockOpen : Lock, shouldRender: canCurrentUserLockPage, @@ -121,8 +121,8 @@ export const PageOptionsDropdown: React.FC = observer((props) => { { key: "archive-restore-page", action: archived_at - ? () => executeCollaborativeAction({ type: "sendMessageToServer", message: "Unarchive" }) - : () => executeCollaborativeAction({ type: "sendMessageToServer", message: "Archive" }), + ? () => executeCollaborativeAction({ type: "sendMessageToServer", message: "unarchive" }) + : () => executeCollaborativeAction({ type: "sendMessageToServer", message: "archive" }), label: archived_at ? "Restore page" : "Archive page", icon: archived_at ? ArchiveRestoreIcon : ArchiveIcon, shouldRender: canCurrentUserArchivePage, diff --git a/web/core/hooks/use-collaborative-page-actions.tsx b/web/core/hooks/use-collaborative-page-actions.tsx index bc66df3eab4..6ec9f799050 100644 --- a/web/core/hooks/use-collaborative-page-actions.tsx +++ b/web/core/hooks/use-collaborative-page-actions.tsx @@ -6,7 +6,7 @@ import { IPage } from "@/store/pages/page"; // Better type naming and structure type CollaborativeAction = { - execute: () => Promise; + execute: (shouldSync?: boolean) => Promise; errorMessage: string; }; @@ -21,24 +21,24 @@ export const useCollaborativePageActions = (editorRef: EditorRefApi | EditorRead const actionHandlerMap: Record = useMemo( () => ({ - [DocumentCollaborativeEvents.Lock.client]: { - execute: page.lock, + [DocumentCollaborativeEvents.lock.client]: { + execute: (shouldSync) => page.lock(shouldSync), errorMessage: "Page could not be locked. Please try again later.", }, - [DocumentCollaborativeEvents.Unlock.client]: { - execute: page.unlock, + [DocumentCollaborativeEvents.unlock.client]: { + execute: (shouldSync) => page.unlock(shouldSync), errorMessage: "Page could not be unlocked. Please try again later.", }, - [DocumentCollaborativeEvents.Archive.client]: { - execute: page.archive, + [DocumentCollaborativeEvents.archive.client]: { + execute: (shouldSync) => page.archive(shouldSync), errorMessage: "Page could not be archived. Please try again later.", }, - [DocumentCollaborativeEvents.Unarchive.client]: { - execute: page.restore, + [DocumentCollaborativeEvents.unarchive.client]: { + execute: (shouldSync) => page.restore(shouldSync), errorMessage: "Page could not be restored. Please try again later.", }, }), - [page.lock, page.unlock, page.archive, page.restore] + [page] ); const executeCollaborativeAction = useCallback( @@ -48,7 +48,7 @@ export const useCollaborativePageActions = (editorRef: EditorRefApi | EditorRead const actionDetails = actionHandlerMap[clientAction]; try { - await actionDetails.execute(); + await actionDetails.execute(isPerformedByCurrentUser); if (isPerformedByCurrentUser) { setCurrentActionBeingProcessed(clientAction); } diff --git a/web/core/store/pages/page.ts b/web/core/store/pages/page.ts index ee4c499b80c..c94734bde10 100644 --- a/web/core/store/pages/page.ts +++ b/web/core/store/pages/page.ts @@ -36,10 +36,10 @@ export interface IPage extends TPage { updateDescription: (document: TDocumentPayload) => Promise; makePublic: () => Promise; makePrivate: () => Promise; - lock: () => Promise; - unlock: () => Promise; - archive: () => Promise; - restore: () => Promise; + lock: (shouldSync?: boolean) => Promise; + unlock: (shouldSync?: boolean) => Promise; + archive: (shouldSync?: boolean) => Promise; + restore: (shouldSync?: boolean) => Promise; updatePageLogo: (logo_props: TLogoProps) => Promise; addToFavorites: () => Promise; removePageFromFavorites: () => Promise; @@ -435,62 +435,94 @@ export class Page implements IPage { /** * @description lock the page */ - lock = async () => { + lock = async (shouldSync: boolean = true) => { const { workspaceSlug, projectId } = this.store.router; if (!workspaceSlug || !projectId || !this.id) return undefined; const pageIsLocked = this.is_locked; runInAction(() => (this.is_locked = true)); - await this.pageService.lock(workspaceSlug, projectId, this.id).catch((error) => { - runInAction(() => { - this.is_locked = pageIsLocked; + if (shouldSync) { + await this.pageService.lock(workspaceSlug, projectId, this.id).catch((error) => { + runInAction(() => { + this.is_locked = pageIsLocked; + }); + throw error; }); - throw error; - }); + } }; /** * @description unlock the page */ - unlock = async () => { + unlock = async (shouldSync: boolean = true) => { const { workspaceSlug, projectId } = this.store.router; if (!workspaceSlug || !projectId || !this.id) return undefined; const pageIsLocked = this.is_locked; runInAction(() => (this.is_locked = false)); - await this.pageService.unlock(workspaceSlug, projectId, this.id).catch((error) => { - runInAction(() => { - this.is_locked = pageIsLocked; + if (shouldSync) { + await this.pageService.unlock(workspaceSlug, projectId, this.id).catch((error) => { + runInAction(() => { + this.is_locked = pageIsLocked; + }); + throw error; }); - throw error; - }); + } }; /** * @description archive the page */ - archive = async () => { + archive = async (shouldSync: boolean = true) => { const { workspaceSlug, projectId } = this.store.router; if (!workspaceSlug || !projectId || !this.id) return undefined; - const response = await this.pageService.archive(workspaceSlug, projectId, this.id); - runInAction(() => { - this.archived_at = response.archived_at; - }); - if (this.rootStore.favorite.entityMap[this.id]) this.rootStore.favorite.removeFavoriteFromStore(this.id); + + try { + runInAction(() => { + this.archived_at = new Date().toISOString(); + }); + + if (this.rootStore.favorite.entityMap[this.id]) this.rootStore.favorite.removeFavoriteFromStore(this.id); + + if (shouldSync) { + const response = await this.pageService.archive(workspaceSlug, projectId, this.id); + runInAction(() => { + this.archived_at = response.archived_at; + }); + } + } catch (error) { + console.error(error); + runInAction(() => { + this.archived_at = null; + }); + } }; /** * @description restore the page */ - restore = async () => { + restore = async (shouldSync: boolean = true) => { const { workspaceSlug, projectId } = this.store.router; if (!workspaceSlug || !projectId || !this.id) return undefined; - await this.pageService.restore(workspaceSlug, projectId, this.id); - runInAction(() => { - this.archived_at = null; - }); + + const archivedAtBeforeRestore = this.archived_at; + + try { + runInAction(() => { + this.archived_at = null; + }); + + if (shouldSync) { + await this.pageService.restore(workspaceSlug, projectId, this.id); + } + } catch (error) { + console.error(error); + runInAction(() => { + this.archived_at = archivedAtBeforeRestore; + }); + } }; updatePageLogo = async (logo_props: TLogoProps) => { From cd26592a87b2fea921333184df8089324c3c2878 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Mon, 2 Dec 2024 14:15:11 +0530 Subject: [PATCH 31/31] chore: removed unused imports --- packages/editor/src/core/hooks/use-editor.ts | 3 +-- packages/editor/src/core/hooks/use-read-only-editor.ts | 8 +++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 4417ba86a32..15fbd19d5c8 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -23,8 +23,7 @@ import type { IMentionSuggestion, TEditorCommands, TFileHandler, - TDocumentEventEmitter, - TExtensions + TExtensions, } from "@/types"; export interface CustomEditorProps { diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index be3ab60e84f..5fb49be5f72 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -11,7 +11,13 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; // props import { CoreReadOnlyEditorProps } from "@/props"; // types -import type { EditorReadOnlyRefApi, IMentionHighlight, TExtensions, TDocumentEventsServer, TFileHandler } from "@/types"; +import type { + EditorReadOnlyRefApi, + IMentionHighlight, + TExtensions, + TDocumentEventsServer, + TFileHandler, +} from "@/types"; interface CustomReadOnlyEditorProps { disabledExtensions: TExtensions[];