diff --git a/apps/web/src/components/EditorPane.tsx b/apps/web/src/components/EditorPane.tsx index d46ec464..fa4c9d54 100644 --- a/apps/web/src/components/EditorPane.tsx +++ b/apps/web/src/components/EditorPane.tsx @@ -50,6 +50,7 @@ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@ import { EditorToolbar } from "./EditorToolbar"; import { RevisionHistoryDialog } from "./dialogs/RevisionHistoryDialog"; import { api } from "@/lib/api"; +import { consumeStandaloneMobileEditorReturn, openStandaloneMobileEditor } from "@/lib/mobile-editor"; import { cn, formatDateTime, parseTagsText } from "@/lib/utils"; import { docToMarkdown, markdownToDoc, type Notebook, type MemoDetail, type TiptapDoc } from "@edgeever/shared"; import { compressImageForUpload } from "@/lib/image-compression"; @@ -930,7 +931,96 @@ const MobileNativeEditorPane = ({ }; export const EditorPane = (props: EditorPaneProps) => { - return ; + const [isMobileViewport, setIsMobileViewport] = useState(() => + typeof window === "undefined" ? false : window.matchMedia(MOBILE_EDITOR_QUERY).matches + ); + const [mobileNativeEditMemoId, setMobileNativeEditMemoId] = useState(null); + const standaloneOpenMemoIdRef = useRef(null); + const readOnly = props.isTrashView || Boolean(props.memo?.isDeleted); + const mobileDefaultEditRequested = Boolean( + props.memo?.id && props.memo.id === props.mobileDefaultEditMemoId && !readOnly + ); + const mobileNativeEditingActive = Boolean( + isMobileViewport && + props.memo && + !readOnly && + (mobileDefaultEditRequested || mobileNativeEditMemoId === props.memo.id) + ); + + useEffect(() => { + if (isMobileViewport && mobileDefaultEditRequested && props.memo?.id) { + if (consumeStandaloneMobileEditorReturn(props.memo.id)) { + props.onMobileDefaultEditConsumed(); + setMobileNativeEditMemoId(null); + props.onBackToList(); + return; + } + + if (standaloneOpenMemoIdRef.current === props.memo.id) { + return; + } + + standaloneOpenMemoIdRef.current = props.memo.id; + props.onMobileDefaultEditConsumed(); + openStandaloneMobileEditor(props.memo.id); + } + }, [isMobileViewport, mobileDefaultEditRequested, props.memo?.id, props.onBackToList, props.onMobileDefaultEditConsumed]); + + useEffect(() => { + const clearReturnedStandaloneEditor = () => { + if (!consumeStandaloneMobileEditorReturn(props.memo?.id ?? null)) { + return; + } + + props.onMobileDefaultEditConsumed(); + setMobileNativeEditMemoId(null); + props.onBackToList(); + }; + + clearReturnedStandaloneEditor(); + window.addEventListener("pageshow", clearReturnedStandaloneEditor); + document.addEventListener("visibilitychange", clearReturnedStandaloneEditor); + + return () => { + window.removeEventListener("pageshow", clearReturnedStandaloneEditor); + document.removeEventListener("visibilitychange", clearReturnedStandaloneEditor); + }; + }, [props.memo?.id, props.onBackToList, props.onMobileDefaultEditConsumed]); + + useEffect(() => { + const mediaQuery = window.matchMedia(MOBILE_EDITOR_QUERY); + const updateMobileViewport = () => setIsMobileViewport(mediaQuery.matches); + + updateMobileViewport(); + mediaQuery.addEventListener("change", updateMobileViewport); + + return () => mediaQuery.removeEventListener("change", updateMobileViewport); + }, []); + + useEffect(() => { + setMobileNativeEditMemoId(null); + }, [props.memo?.id]); + + if (mobileNativeEditingActive) { + return ( +
+ 打开编辑器 +
+ ); + } + + return ( + { + if (props.memo?.id && !readOnly) { + setMobileNativeEditMemoId(props.memo.id); + openStandaloneMobileEditor(props.memo.id); + } + }} + /> + ); }; const RichEditorPane = ({ diff --git a/apps/web/src/components/MobileStandaloneTiptapEditor.tsx b/apps/web/src/components/MobileStandaloneTiptapEditor.tsx index f3b8cb1b..e19b1d07 100644 --- a/apps/web/src/components/MobileStandaloneTiptapEditor.tsx +++ b/apps/web/src/components/MobileStandaloneTiptapEditor.tsx @@ -14,7 +14,7 @@ import { import { getNotebookMoveOptions } from "@/lib/app-helpers"; import { compressImageForUpload } from "@/lib/image-compression"; import { localDb, type LocalDraft } from "@/lib/local-db"; -import { markStandaloneMobileEditorReturning } from "@/lib/mobile-editor"; +import { getStandaloneMobileEditorReturnPath, markStandaloneMobileEditorReturning } from "@/lib/mobile-editor"; import { DEFAULT_MOBILE_EDITOR_MEMO_TITLE, MOBILE_EDITOR_AUTO_SAVE_DELAY_MS, @@ -27,7 +27,6 @@ import { normalizeMobileEditorDoc, parseMobileEditorTags, requestMobileEditorJson, - safeMobileEditorReturnPath, uploadMobileEditorResource, type MobileEditorDraft, type MobileEditorMemoResponse, @@ -41,18 +40,15 @@ type ListNotebooksResponse = { type MobileStandaloneTiptapEditorProps = { memoId?: string | null; - returnTo?: string; onLeave?: () => void; }; export const MobileStandaloneTiptapEditor = ({ memoId: memoIdProp, - returnTo: returnToProp, onLeave, }: MobileStandaloneTiptapEditorProps = {}) => { const params = useMemo(() => getMobileEditorParams(), []); const memoId = memoIdProp ?? params.get("memoId"); - const returnTo = safeMobileEditorReturnPath(returnToProp ?? params.get("returnTo")); const draftKey = getMobileEditorDraftKey(memoId); const [memo, setMemo] = useState(null); const memoRef = useRef(null); @@ -440,8 +436,8 @@ export const MobileStandaloneTiptapEditor = ({ return; } - window.location.replace(returnTo); - }, [memoId, onLeave, returnTo]); + window.location.replace(memoId ? getStandaloneMobileEditorReturnPath(memoId) : "/"); + }, [memoId, onLeave]); const leavePage = useCallback(async () => { if (leavingRef.current) { diff --git a/apps/web/src/components/WorkspaceApp.tsx b/apps/web/src/components/WorkspaceApp.tsx index 2d91c466..746f0fd1 100644 --- a/apps/web/src/components/WorkspaceApp.tsx +++ b/apps/web/src/components/WorkspaceApp.tsx @@ -27,7 +27,7 @@ import { import { MemoListPane, MemoSelectionActionBar } from "./MemoListPane"; import { AppConfirmDialog, MemoDeleteConfirmDialog, NotebookNameDialog } from "./dialogs/ConfirmDialogs"; import { api } from "@/lib/api"; -import { MOBILE_EDITOR_RETURN_PARAM } from "@/lib/mobile-editor"; +import { MOBILE_EDITOR_RETURN_PARAM, openStandaloneMobileEditor } from "@/lib/mobile-editor"; import { cn } from "@/lib/utils"; import { createExcerpt, docToText, type Notebook, type AuthUser, type MemoSummary, type MemoDetail } from "@edgeever/shared"; import type { @@ -1296,6 +1296,9 @@ export const WorkspaceApp = ({ setSelectedMemoId(data.memo.id); setActivePane("editor"); + if (!isDesktopViewport()) { + openStandaloneMobileEditor(data.memo.id); + } }, }); diff --git a/apps/web/src/mobile-edit.tsx b/apps/web/src/mobile-edit.tsx index ccbdc3b5..a37b0696 100644 --- a/apps/web/src/mobile-edit.tsx +++ b/apps/web/src/mobile-edit.tsx @@ -1,4 +1,16 @@ -const params = new URLSearchParams(window.location.hash ? window.location.hash.slice(1) : window.location.search); -const returnTo = params.get("returnTo"); +import React from "react"; +import { createRoot } from "react-dom/client"; +import { MobileStandaloneTiptapEditor } from "@/components/MobileStandaloneTiptapEditor"; +import "./styles/mobile-markdown-editor.css"; -window.location.replace(returnTo?.startsWith("/") ? returnTo : "/"); +const root = document.getElementById("mobile-editor-root"); + +if (!root) { + throw new Error("Mobile editor root not found"); +} + +createRoot(root).render( + + + +);