mirror of
https://github.com/docmost/docmost.git
synced 2026-05-18 07:24:04 +08:00
fix editor flickers on collab reconnection (#1295)
* fix editor flickers on reconnection * cleanup * adjust copy
This commit is contained in:
@@ -154,7 +154,7 @@ export default function BillingDetails() {
|
|||||||
Current Tier
|
Current Tier
|
||||||
</Text>
|
</Text>
|
||||||
<Text fw={700} fz="lg">
|
<Text fw={700} fz="lg">
|
||||||
For up to {billing.tieredUpTo} users
|
For {billing.tieredUpTo} users
|
||||||
</Text>
|
</Text>
|
||||||
{/*billing.tieredFlatAmount && (
|
{/*billing.tieredFlatAmount && (
|
||||||
<Text c="dimmed" fz="sm">
|
<Text c="dimmed" fz="sm">
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export default function BillingPlans() {
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Text size="md" fw={500}>
|
<Text size="md" fw={500}>
|
||||||
for up to {planSelectedTier.upTo} users
|
For {planSelectedTier.upTo} users
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import "@/features/editor/styles/index.css";
|
import "@/features/editor/styles/index.css";
|
||||||
import React, {
|
import React, {
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
@@ -72,7 +71,11 @@ export default function PageEditor({
|
|||||||
const [, setAsideState] = useAtom(asideStateAtom);
|
const [, setAsideState] = useAtom(asideStateAtom);
|
||||||
const [, setActiveCommentId] = useAtom(activeCommentIdAtom);
|
const [, setActiveCommentId] = useAtom(activeCommentIdAtom);
|
||||||
const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
||||||
const ydoc = useMemo(() => new Y.Doc(), [pageId]);
|
const ydocRef = useRef<Y.Doc | null>(null);
|
||||||
|
if (!ydocRef.current) {
|
||||||
|
ydocRef.current = new Y.Doc();
|
||||||
|
}
|
||||||
|
const ydoc = ydocRef.current;
|
||||||
const [isLocalSynced, setLocalSynced] = useState(false);
|
const [isLocalSynced, setLocalSynced] = useState(false);
|
||||||
const [isRemoteSynced, setRemoteSynced] = useState(false);
|
const [isRemoteSynced, setRemoteSynced] = useState(false);
|
||||||
const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom(
|
const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom(
|
||||||
@@ -89,66 +92,100 @@ export default function PageEditor({
|
|||||||
const userPageEditMode =
|
const userPageEditMode =
|
||||||
currentUser?.user?.settings?.preferences?.pageEditMode ?? PageEditMode.Edit;
|
currentUser?.user?.settings?.preferences?.pageEditMode ?? PageEditMode.Edit;
|
||||||
|
|
||||||
const localProvider = useMemo(() => {
|
// Providers only created once per pageId
|
||||||
const provider = new IndexeddbPersistence(documentName, ydoc);
|
const providersRef = useRef<{
|
||||||
|
local: IndexeddbPersistence;
|
||||||
|
remote: HocuspocusProvider;
|
||||||
|
} | null>(null);
|
||||||
|
const [providersReady, setProvidersReady] = useState(false);
|
||||||
|
|
||||||
provider.on("synced", () => {
|
const localProvider = providersRef.current?.local;
|
||||||
setLocalSynced(true);
|
const remoteProvider = providersRef.current?.remote;
|
||||||
});
|
|
||||||
|
|
||||||
return provider;
|
// Track when collaborative provider is ready and synced
|
||||||
}, [pageId, ydoc]);
|
const [collabReady, setCollabReady] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
remoteProvider?.status === WebSocketStatus.Connected &&
|
||||||
|
isLocalSynced &&
|
||||||
|
isRemoteSynced
|
||||||
|
) {
|
||||||
|
setCollabReady(true);
|
||||||
|
}
|
||||||
|
}, [remoteProvider?.status, isLocalSynced, isRemoteSynced]);
|
||||||
|
|
||||||
const remoteProvider = useMemo(() => {
|
useEffect(() => {
|
||||||
const provider = new HocuspocusProvider({
|
if (!providersRef.current) {
|
||||||
name: documentName,
|
const local = new IndexeddbPersistence(documentName, ydoc);
|
||||||
url: collaborationURL,
|
local.on("synced", () => setLocalSynced(true));
|
||||||
document: ydoc,
|
const remote = new HocuspocusProvider({
|
||||||
token: collabQuery?.token,
|
name: documentName,
|
||||||
connect: false,
|
url: collaborationURL,
|
||||||
preserveConnection: false,
|
document: ydoc,
|
||||||
onAuthenticationFailed: (auth: onAuthenticationFailedParameters) => {
|
token: collabQuery?.token,
|
||||||
const payload = jwtDecode(collabQuery?.token);
|
connect: true,
|
||||||
const now = Date.now().valueOf() / 1000;
|
preserveConnection: false,
|
||||||
const isTokenExpired = now >= payload.exp;
|
onAuthenticationFailed: (auth: onAuthenticationFailedParameters) => {
|
||||||
if (isTokenExpired) {
|
const payload = jwtDecode(collabQuery?.token);
|
||||||
refetchCollabToken();
|
const now = Date.now().valueOf() / 1000;
|
||||||
}
|
const isTokenExpired = now >= payload.exp;
|
||||||
},
|
if (isTokenExpired) {
|
||||||
onStatus: (status) => {
|
refetchCollabToken();
|
||||||
if (status.status === "connected") {
|
}
|
||||||
setYjsConnectionStatus(status.status);
|
},
|
||||||
}
|
onStatus: (status) => {
|
||||||
},
|
if (status.status === "connected") {
|
||||||
});
|
setYjsConnectionStatus(status.status);
|
||||||
|
}
|
||||||
provider.on("synced", () => {
|
},
|
||||||
setRemoteSynced(true);
|
});
|
||||||
});
|
remote.on("synced", () => setRemoteSynced(true));
|
||||||
|
remote.on("disconnect", () => {
|
||||||
provider.on("disconnect", () => {
|
setYjsConnectionStatus(WebSocketStatus.Disconnected);
|
||||||
setYjsConnectionStatus(WebSocketStatus.Disconnected);
|
});
|
||||||
});
|
providersRef.current = { local, remote };
|
||||||
|
setProvidersReady(true);
|
||||||
return provider;
|
} else {
|
||||||
}, [ydoc, pageId, collabQuery?.token]);
|
setProvidersReady(true);
|
||||||
|
}
|
||||||
useLayoutEffect(() => {
|
// Only destroy on final unmount
|
||||||
remoteProvider.connect();
|
|
||||||
return () => {
|
return () => {
|
||||||
setRemoteSynced(false);
|
providersRef.current?.remote.destroy();
|
||||||
setLocalSynced(false);
|
providersRef.current?.local.destroy();
|
||||||
remoteProvider.destroy();
|
providersRef.current = null;
|
||||||
localProvider.destroy();
|
|
||||||
};
|
};
|
||||||
}, [remoteProvider, localProvider]);
|
}, [pageId]);
|
||||||
|
|
||||||
|
// Only connect/disconnect on tab/idle, not destroy
|
||||||
|
useEffect(() => {
|
||||||
|
if (!providersReady || !providersRef.current) return;
|
||||||
|
const remoteProvider = providersRef.current.remote;
|
||||||
|
if (
|
||||||
|
isIdle &&
|
||||||
|
documentState === "hidden" &&
|
||||||
|
remoteProvider.status === WebSocketStatus.Connected
|
||||||
|
) {
|
||||||
|
remoteProvider.disconnect();
|
||||||
|
setIsCollabReady(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
documentState === "visible" &&
|
||||||
|
remoteProvider.status === WebSocketStatus.Disconnected
|
||||||
|
) {
|
||||||
|
resetIdle();
|
||||||
|
remoteProvider.connect();
|
||||||
|
setTimeout(() => setIsCollabReady(true), 500);
|
||||||
|
}
|
||||||
|
}, [isIdle, documentState, providersReady, resetIdle]);
|
||||||
|
|
||||||
const extensions = useMemo(() => {
|
const extensions = useMemo(() => {
|
||||||
|
if (!remoteProvider || !currentUser?.user) return mainExtensions;
|
||||||
return [
|
return [
|
||||||
...mainExtensions,
|
...mainExtensions,
|
||||||
...collabExtensions(remoteProvider, currentUser?.user),
|
...collabExtensions(remoteProvider, currentUser?.user),
|
||||||
];
|
];
|
||||||
}, [ydoc, pageId, remoteProvider, currentUser?.user]);
|
}, [remoteProvider, currentUser?.user]);
|
||||||
|
|
||||||
const editor = useEditor(
|
const editor = useEditor(
|
||||||
{
|
{
|
||||||
@@ -202,7 +239,7 @@ export default function PageEditor({
|
|||||||
debouncedUpdateContent(editorJson);
|
debouncedUpdateContent(editorJson);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[pageId, editable, remoteProvider?.status],
|
[pageId, editable, remoteProvider],
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedUpdateContent = useDebouncedCallback((newContent: any) => {
|
const debouncedUpdateContent = useDebouncedCallback((newContent: any) => {
|
||||||
@@ -255,29 +292,6 @@ export default function PageEditor({
|
|||||||
}
|
}
|
||||||
}, [remoteProvider?.status]);
|
}, [remoteProvider?.status]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
isIdle &&
|
|
||||||
documentState === "hidden" &&
|
|
||||||
remoteProvider?.status === WebSocketStatus.Connected
|
|
||||||
) {
|
|
||||||
remoteProvider.disconnect();
|
|
||||||
setIsCollabReady(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
documentState === "visible" &&
|
|
||||||
remoteProvider?.status === WebSocketStatus.Disconnected
|
|
||||||
) {
|
|
||||||
resetIdle();
|
|
||||||
remoteProvider.connect();
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsCollabReady(true);
|
|
||||||
}, 600);
|
|
||||||
}
|
|
||||||
}, [isIdle, documentState, remoteProvider]);
|
|
||||||
|
|
||||||
const isSynced = isLocalSynced && isRemoteSynced;
|
const isSynced = isLocalSynced && isRemoteSynced;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -294,21 +308,48 @@ export default function PageEditor({
|
|||||||
}, [isRemoteSynced, isLocalSynced, remoteProvider?.status]);
|
}, [isRemoteSynced, isLocalSynced, remoteProvider?.status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// honor user default page edit mode preference
|
// Only honor user default page edit mode preference and permissions
|
||||||
if (userPageEditMode && editor && editable && isSynced) {
|
if (editor) {
|
||||||
if (userPageEditMode === PageEditMode.Edit) {
|
if (userPageEditMode && editable) {
|
||||||
editor.setEditable(true);
|
if (userPageEditMode === PageEditMode.Edit) {
|
||||||
} else if (userPageEditMode === PageEditMode.Read) {
|
editor.setEditable(true);
|
||||||
|
} else if (userPageEditMode === PageEditMode.Read) {
|
||||||
|
editor.setEditable(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
editor.setEditable(false);
|
editor.setEditable(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [userPageEditMode, editor, editable, isSynced]);
|
}, [userPageEditMode, editor, editable]);
|
||||||
|
|
||||||
return isCollabReady ? (
|
const hasConnectedOnceRef = useRef(false);
|
||||||
<div>
|
const [showStatic, setShowStatic] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!hasConnectedOnceRef.current &&
|
||||||
|
remoteProvider?.status === WebSocketStatus.Connected
|
||||||
|
) {
|
||||||
|
hasConnectedOnceRef.current = true;
|
||||||
|
setShowStatic(false);
|
||||||
|
}
|
||||||
|
}, [remoteProvider?.status]);
|
||||||
|
|
||||||
|
if (showStatic) {
|
||||||
|
return (
|
||||||
|
<EditorProvider
|
||||||
|
editable={false}
|
||||||
|
immediatelyRender={true}
|
||||||
|
extensions={mainExtensions}
|
||||||
|
content={content}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
<div ref={menuContainerRef}>
|
<div ref={menuContainerRef}>
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
|
|
||||||
{editor && editor.isEditable && (
|
{editor && editor.isEditable && (
|
||||||
<div>
|
<div>
|
||||||
<EditorBubbleMenu editor={editor} />
|
<EditorBubbleMenu editor={editor} />
|
||||||
@@ -322,21 +363,12 @@ export default function PageEditor({
|
|||||||
<LinkMenu editor={editor} appendTo={menuContainerRef} />
|
<LinkMenu editor={editor} appendTo={menuContainerRef} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showCommentPopup && <CommentDialog editor={editor} pageId={pageId} />}
|
{showCommentPopup && <CommentDialog editor={editor} pageId={pageId} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onClick={() => editor.commands.focus("end")}
|
onClick={() => editor.commands.focus("end")}
|
||||||
style={{ paddingBottom: "20vh" }}
|
style={{ paddingBottom: "20vh" }}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<EditorProvider
|
|
||||||
editable={false}
|
|
||||||
immediatelyRender={true}
|
|
||||||
extensions={mainExtensions}
|
|
||||||
content={content}
|
|
||||||
></EditorProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user