diff --git a/apps/ios/EdgeEver/Editor/TipTapWarmPool.swift b/apps/ios/EdgeEver/Editor/TipTapWarmPool.swift index b92f097c..52b4091e 100644 --- a/apps/ios/EdgeEver/Editor/TipTapWarmPool.swift +++ b/apps/ios/EdgeEver/Editor/TipTapWarmPool.swift @@ -142,7 +142,7 @@ final class SharedTipTapRuntime: NSObject, WKScriptMessageHandler, WKNavigationD needsForcePushOnBind = true } // Drop action callbacks for the dismantled SwiftUI host so late JS `change` - // events cannot rewrite the next create draft with this session's body. + // events cannot rewrite a later editor session with this session's body. if var s = session { s.onChange = nil s.onResourcePress = nil @@ -635,7 +635,7 @@ final class SharedTipTapRuntime: NSObject, WKScriptMessageHandler, WKNavigationD pushContentIfNeeded(force: true) case "change": // Host dismantled (create/edit dismissed) — drop late events so they cannot - // resurrect the previous body into a new-note draft via onChange autosave. + // resurrect the previous body in a later editor session. guard let session, session.onChange != nil else { return } let md = body["contentMarkdown"] as? String ?? "" let json = body["contentJson"] as? String ?? session.documentJSON diff --git a/apps/ios/EdgeEver/Features/Workspace/MemoEditView.swift b/apps/ios/EdgeEver/Features/Workspace/MemoEditView.swift index 6f0bc89f..e6d3b46c 100644 --- a/apps/ios/EdgeEver/Features/Workspace/MemoEditView.swift +++ b/apps/ios/EdgeEver/Features/Workspace/MemoEditView.swift @@ -9,7 +9,8 @@ private enum ImagePickerRoute: String, Identifiable { } enum MemoEditMode: Equatable { - /// `seed` pre-fills title/body/tags (template / clip-style create). When nil, restores local new-note draft. + /// `seed` pre-fills title/body/tags for an explicit template, clip, or share flow. + /// A regular create always starts blank. case create(notebookId: String, seed: CreateMemoSeed? = nil) case edit(memoId: String) } @@ -781,19 +782,23 @@ struct MemoEditView: View { guard let scope = env.session.dataScope else { return } switch mode { case .create(let nb, let seed): + // A regular create must never inherit state from the previous create + // session. Clear both in-memory fields and any legacy persisted draft. + title = "" + tagsText = "" + contentMarkdown = "" + contentJSON = MemoEditViewModel.emptyDocJSON notebookId = nb + memoId = nil + expectedRevision = nil + expectedContentHash = nil + try? env.drafts.clear(scope: scope, key: DraftRepository.newKey) if let seed { - // Template / explicit seed wins over local new-note draft (Android initialDraft). + // Only explicit template / clip / share input may prefill a create. title = seed.title tagsText = seed.tagsText contentMarkdown = seed.contentMarkdown contentJSON = MemoEditViewModel.emptyDocJSON - } else if let draft = try? env.drafts.read(scope: scope, key: DraftRepository.newKey) { - title = draft.title - tagsText = draft.tagsText - contentMarkdown = draft.contentMarkdown - contentJSON = draft.contentJson ?? contentJSON - if !draft.notebookId.isEmpty { notebookId = draft.notebookId } } baselineMarkdown = contentMarkdown case .edit(let id): @@ -893,29 +898,14 @@ struct MemoEditView: View { } let now = EdgeEverDate.nowString() - // Create mode before materialize: draft only. - // Create mode after materialize (image upload) OR edit: mirror + outbox update. + // A non-materialized create remains in memory until Done/Back commits it. + // Persisting it under the shared `new` key would leak this content into a + // later create session. if isCreate, !hasMaterializedServerMemo { - try? env.drafts.write( - scope: scope, - draft: viewModel.makeDraft(key: DraftRepository.newKey, expectedRevision: nil, updatedAt: now) - ) - NSLog( - "MemoEditView persist draft-only mdLen=%d hasImg=%d", - contentMarkdown.count, - contentMarkdown.contains("/api/v1/resources/") ? 1 : 0 - ) return } guard let memoId, !memoId.hasPrefix("local:") else { - // Offline local: create still in flight — keep draft. - if isCreate { - try? env.drafts.write( - scope: scope, - draft: viewModel.makeDraft(key: DraftRepository.newKey, expectedRevision: nil, updatedAt: now) - ) - } return } @@ -955,14 +945,16 @@ struct MemoEditView: View { ) expectedRevision = rev expectedContentHash = hash - try? env.drafts.write( - scope: scope, - draft: viewModel.makeDraft( - key: isCreate ? DraftRepository.newKey : DraftRepository.memoKey(memo.id), - expectedRevision: rev, - updatedAt: now + if !isCreate { + try? env.drafts.write( + scope: scope, + draft: viewModel.makeDraft( + key: DraftRepository.memoKey(memo.id), + expectedRevision: rev, + updatedAt: now + ) ) - ) + } NSLog( "MemoEditView persist update memo=%@ baseRev=%d mdLen=%d hasImg=%d jsonHasImg=%d", memo.id, diff --git a/apps/ios/Tests/EdgeEverTests/ChromeParityTests.swift b/apps/ios/Tests/EdgeEverTests/ChromeParityTests.swift index 2c1c9e7f..ac5facd4 100644 --- a/apps/ios/Tests/EdgeEverTests/ChromeParityTests.swift +++ b/apps/ios/Tests/EdgeEverTests/ChromeParityTests.swift @@ -119,6 +119,23 @@ final class ChromeParityTests: XCTestCase { XCTAssertFalse(src.contains("ToolbarItem(placement: .cancellationAction)"), "system Close toolbar not Android create chrome") } + func testRegularCreateNeverRestoresOrPersistsPreviousCreateContent() throws { + let src = try readShippedSource("Features/Workspace/MemoEditView.swift") + + XCTAssertFalse( + src.contains("env.drafts.read(scope: scope, key: DraftRepository.newKey)"), + "regular create must not restore the previous new-note draft" + ) + XCTAssertFalse( + src.contains("makeDraft(key: DraftRepository.newKey"), + "regular create must not persist content for a later create session" + ) + XCTAssertTrue( + src.contains("env.drafts.clear(scope: scope, key: DraftRepository.newKey)"), + "regular create must remove legacy new-note drafts" + ) + } + func testMemoDetailViewUsesDetailChromeAndEditFab() throws { let src = try readShippedSource("Features/Workspace/MemoDetailView.swift") XCTAssertTrue(src.contains("DetailMemoChrome.header") || src.contains("detailHeader"), src)