Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
448a615
fix: add lock unlock archive restore realtime sync
Palanikannan1437 Sep 17, 2024
cd29430
fix: show only after editor loads
Palanikannan1437 Sep 17, 2024
8ce39f5
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Sep 23, 2024
c94fff9
fix: added strong types
Palanikannan1437 Sep 23, 2024
2c2dd62
fix: live events fixed
Palanikannan1437 Sep 24, 2024
c322122
fix: remove unused vars and logs
Palanikannan1437 Sep 24, 2024
e3d3ab5
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Oct 1, 2024
83933ec
fix: converted objects to enum
Palanikannan1437 Oct 3, 2024
6f13c19
fix: error handling and removing the events in read only mode
Palanikannan1437 Oct 3, 2024
5a835e4
fix: added check to only update if the image aspect ratio is not pres…
Palanikannan1437 Oct 3, 2024
1732587
fix: imports
Palanikannan1437 Oct 3, 2024
20861a6
fix: props order
Palanikannan1437 Oct 3, 2024
ed751e3
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Oct 3, 2024
2384f0b
revert: no need of these changes anymore
Palanikannan1437 Oct 3, 2024
bbe7e62
fix: updated type names
Palanikannan1437 Oct 3, 2024
b0bf242
fix: order of things
Palanikannan1437 Oct 3, 2024
b8c76eb
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Oct 8, 2024
b42f552
fix: fixed types and renamed variables
Palanikannan1437 Oct 8, 2024
702236e
fix: better typing for the real time updates
Palanikannan1437 Oct 8, 2024
ee9e7f5
fix: trying multiplexing our socket connection
Palanikannan1437 Oct 8, 2024
b45761e
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Oct 18, 2024
2a22f01
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Oct 30, 2024
9f0ca0d
fix: multiplexing socket connection in read only editor as well
Palanikannan1437 Nov 7, 2024
fa53baf
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Nov 7, 2024
05474e9
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Nov 18, 2024
e846f6f
fix: remove single socket logic
Palanikannan1437 Nov 18, 2024
bb45ad2
fix: fixing the cleanup deps for the provider and localprovider
Palanikannan1437 Nov 18, 2024
a4e1b67
fix: add a better data structure for managing events
Palanikannan1437 Nov 19, 2024
d16aff9
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Nov 19, 2024
842564a
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Nov 21, 2024
bb715d2
chore: refactored realtime events into hooks
Palanikannan1437 Nov 22, 2024
5362a2c
feat: fetch page meta while focusing tabs
Palanikannan1437 Nov 22, 2024
37197c1
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Nov 26, 2024
e8d4500
fix: cycling through items on slash command item in down arrow
Palanikannan1437 Nov 26, 2024
7bc9ff2
fix: better naming convention for realtime events
Palanikannan1437 Nov 26, 2024
0626f16
fix: simplified localprovider initialization and cleaning
Palanikannan1437 Nov 26, 2024
2d648c7
fix: types from ui
Palanikannan1437 Nov 26, 2024
6c9bcf1
fix: abstracted away from exposing the provider directly
Palanikannan1437 Nov 26, 2024
173e93f
fix: coderabbit suggestions
Palanikannan1437 Nov 26, 2024
58178cf
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Nov 26, 2024
1a66f3c
regression: pass user in dependency array
Palanikannan1437 Nov 26, 2024
429bbc4
fix: removed page action api calls by the other users the document is…
Palanikannan1437 Nov 26, 2024
bc9a09d
Merge branch 'preview' into fix/lock-unlock-realtime
Palanikannan1437 Dec 2, 2024
cd26592
chore: removed unused imports
Palanikannan1437 Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion live/src/core/hocuspocus-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -37,7 +41,13 @@ export const getHocusPocusServer = async () => {
throw Error("Authentication unsuccessful!");
}
},
async onStateless({ payload, document }) {
const response = documentEventResponses[payload as DocumentEventsServer];
if (response) {
document.broadcastStateless(response);
}
},
extensions,
debounce: 10000
debounce: 10000,
});
};
9 changes: 9 additions & 0 deletions packages/editor/src/core/helpers/document-events.ts
Original file line number Diff line number Diff line change
@@ -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];
3 changes: 3 additions & 0 deletions packages/editor/src/core/hooks/use-collaborative-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
onClose: (data) => {
if (data.event.code === 1006) serverHandler?.onServerError?.();
},
onSynced: () => {
serverHandler?.onSynced?.();
},
}),
[id, realtimeConfig, serverHandler, user.id]
);
Expand Down
9 changes: 6 additions & 3 deletions packages/editor/src/core/hooks/use-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
import { CoreEditorProps } from "@/props";
// types
import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TEditorCommands, TFileHandler } from "@/types";
import { DocumentEventsServer } from "src/lib";

export interface CustomEditorProps {
editorClassName: string;
Expand All @@ -34,11 +35,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) => {
Expand All @@ -55,9 +56,9 @@ export const useEditor = (props: CustomEditorProps) => {
mentionHandler,
onChange,
placeholder,
provider,
tabIndex,
value,
provider,
} = props;
// states
const [savedSelection, setSavedSelection] = useState<Selection | null>(null);
Expand Down Expand Up @@ -230,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);
Expand Down Expand Up @@ -271,6 +272,8 @@ export const useEditor = (props: CustomEditorProps) => {
if (!document) return;
Y.applyUpdate(document, value);
},
emitRealTimeUpdate: (message: DocumentEventsServer) => provider?.sendStateless(message),
listenToRealTimeUpdate: () => provider,
}),
[editorRef, savedSelection]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit
onClose: (data) => {
if (data.event.code === 1006) serverHandler?.onServerError?.();
},
onSynced: () => {
serverHandler?.onSynced?.();
},
}),
[id, realtimeConfig, user.id]
[id, realtimeConfig, serverHandler, user.id]
);

// destroy and disconnect connection on unmount
useEffect(
() => () => {
Expand Down
8 changes: 8 additions & 0 deletions packages/editor/src/core/hooks/use-read-only-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
editorRef.current?.off("update");
};
},
emitRealTimeUpdate: (message: string) => {
if (provider) {
provider.sendStateless(message);
}
},
listenToRealTimeUpdate: () => {
return provider;
},
getHeadings: () => editorRef?.current?.storage.headingList.headings,
}));

Expand Down
1 change: 1 addition & 0 deletions packages/editor/src/core/types/collaboration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
export type TServerHandler = {
onConnect?: () => void;
onServerError?: () => void;
onSynced?: () => void;
};

type TCollaborativeEditorHookProps = {
Expand Down
6 changes: 4 additions & 2 deletions packages/editor/src/core/types/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
TFileHandler,
TServerHandler,
} from "@/types";
import { DocumentEventsServer } from "src/lib";
import { HocuspocusProvider } from "@hocuspocus/provider";

// editor refs
export type EditorReadOnlyRefApi = {
Expand All @@ -30,8 +32,8 @@ export type EditorReadOnlyRefApi = {
paragraphs: number;
words: number;
};
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
getHeadings: () => IMarking[];
emitRealTimeUpdate: (message: DocumentEventsServer) => void;
listenToRealTimeUpdate: () => HocuspocusProvider;
};

export interface EditorRefApi extends EditorReadOnlyRefApi {
Expand Down
1 change: 1 addition & 0 deletions packages/editor/src/lib.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "@/extensions/core-without-props";
export * from "@/helpers/document-events";
13 changes: 10 additions & 3 deletions web/core/components/pages/editor/editor-body.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useMemo } from "react";
import { useCallback, useMemo, useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// document-editor
Expand All @@ -7,7 +7,6 @@ import {
CollaborativeDocumentReadOnlyEditorWithRef,
EditorReadOnlyRefApi,
EditorRefApi,
IMarking,
TAIMenuProps,
TDisplayConfig,
TRealtimeConfig,
Expand Down Expand Up @@ -57,6 +56,9 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
readOnlyEditorRef,
sidePeekVisible,
} = props;
// states
const [isSynced, setIsSynced] = useState(false);

// router
const { workspaceSlug, projectId } = useParams();
// store hooks
Expand All @@ -67,7 +69,7 @@ export const PageEditorBody: React.FC<Props> = 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;
Expand Down Expand Up @@ -105,10 +107,15 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
handleConnectionStatus(true);
}, []);

const handleServerSynced = useCallback(() => {
setIsSynced(true);
}, []);

const serverHandler: TServerHandler = useMemo(
() => ({
onConnect: handleServerConnect,
onServerError: handleServerError,
onSynced: handleServerSynced,
}),
[]
);
Expand Down
144 changes: 113 additions & 31 deletions web/core/components/pages/editor/header/options-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

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";
import { DocumentEventsClient } from "@plane/editor/lib";
// ui
import { ArchiveIcon, CustomMenu, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
// helpers
Expand All @@ -23,6 +25,42 @@ type Props = {

export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
const { editorRef, handleDuplicatePage, page } = props;
// create a local state to track if the current action is being processed
const [localAction, setLocalAction] = useState<string | null>(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
Expand All @@ -45,41 +83,85 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
// update query params
const { updateQueryParams } = useQueryParams();

const handleArchivePage = async () =>
await archive().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be archived. Please try again later.",
const handleArchivePage = async (isLocal: boolean = true) => {
await archive()
.then(() => {
if (isLocal) {
setLocalAction("archived");
}
})
);

const handleRestorePage = async () =>
await restore().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be restored. Please try again later.",
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be archived. Please try again later.",
});
});
};

// 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 = async (isLocal: boolean = true) => {
await restore()
.then(() => {
if (isLocal) {
setLocalAction("unarchived");
}
})
);

const handleLockPage = async () =>
await lock().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be locked. Please try again later.",
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be restored. Please try again later.",
})
);
};

const handleLockPage = async (isLocal: boolean = true) => {
await lock()
.then(() => {
if (isLocal) {
setLocalAction("locked");
}
})
);

const handleUnlockPage = async () =>
await unlock().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be unlocked. Please try again later.",
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Page could not be locked. Please try again later.",
})
);
};

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.",
})
);
};

// menu items list
const MENU_ITEMS: {
Expand Down