144 lines
5.9 KiB
Swift
144 lines
5.9 KiB
Swift
import Foundation
|
|
|
|
/// Shipped create-Done path for `MemoEditView` (Android `createMutation` parity).
|
|
///
|
|
/// After image upload, `materializeForImage` already creates a **server** memo and sets
|
|
/// `memoId`. Done must **update** that memo — never mint a second `local:` + create.
|
|
enum MemoCreateCommit {
|
|
enum Outcome: Equatable, Sendable {
|
|
case createdLocal(memoId: String)
|
|
case updatedMaterialized(memoId: String)
|
|
}
|
|
|
|
/// Server id from materialize (not offline `local:` placeholder).
|
|
static func isMaterializedServerId(_ memoId: String?) -> Bool {
|
|
guard let memoId else { return false }
|
|
let trimmed = memoId.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else { return false }
|
|
return !trimmed.hasPrefix("local:")
|
|
}
|
|
|
|
/// Commit create modal content.
|
|
/// - If `memoId` is a materialized server id → optimistic mirror update + outbox update.
|
|
/// - Else → offline-first `local:` create + outbox create (Android temporaryId path).
|
|
@discardableResult
|
|
static func commit(
|
|
scope: String,
|
|
memoId: String?,
|
|
expectedRevision: Int?,
|
|
expectedContentHash: String?,
|
|
notebookId: String,
|
|
title: String,
|
|
untitledTitle: String = "无标题笔记",
|
|
contentMarkdown: String,
|
|
contentJSON: String?,
|
|
tags: [String],
|
|
mirror: LocalMirrorRepository,
|
|
outbox: SyncOutboxRepository,
|
|
drafts: DraftRepository,
|
|
now: String = EdgeEverDate.nowString()
|
|
) throws -> Outcome {
|
|
guard !notebookId.isEmpty else {
|
|
throw APIError(status: 400, code: nil, message: "请选择笔记本")
|
|
}
|
|
let displayTitle = title.isEmpty ? untitledTitle : title
|
|
|
|
if isMaterializedServerId(memoId), let serverId = memoId {
|
|
guard var memo = try mirror.resolveMemo(scope: scope, id: serverId) else {
|
|
// Mirror miss: still queue update against known server id (Android upserts optimistic).
|
|
var placeholder = MemoDetail.localPlaceholder(
|
|
id: serverId,
|
|
notebookId: notebookId,
|
|
title: displayTitle,
|
|
contentMarkdown: contentMarkdown,
|
|
tags: tags,
|
|
createdAt: now
|
|
)
|
|
placeholder.revision = expectedRevision ?? 0
|
|
placeholder.contentHash = expectedContentHash ?? "local:\(serverId)"
|
|
if let contentJSON, let json = try? JSONValue.parse(contentJSON) {
|
|
placeholder.contentJson = json
|
|
}
|
|
try mirror.upsertMemo(scope: scope, memo: placeholder)
|
|
try outbox.enqueueUpdate(
|
|
scope: scope,
|
|
payload: MemoUpdatePayload(
|
|
memoId: serverId,
|
|
expectedRevision: expectedRevision ?? 0,
|
|
expectedContentHash: expectedContentHash ?? placeholder.contentHash,
|
|
title: displayTitle,
|
|
contentMarkdown: contentMarkdown,
|
|
contentJson: contentJSON,
|
|
notebookId: notebookId,
|
|
tags: tags
|
|
)
|
|
)
|
|
try drafts.clear(scope: scope, key: DraftRepository.newKey)
|
|
try? drafts.clear(scope: scope, key: DraftRepository.memoKey(serverId))
|
|
return .updatedMaterialized(memoId: serverId)
|
|
}
|
|
|
|
// Use mirror base (server tip). Stale caller expectedRevision causes 409 conflicts.
|
|
let rev = memo.revision
|
|
let hash = memo.contentHash
|
|
memo.title = displayTitle
|
|
memo.contentMarkdown = contentMarkdown
|
|
memo.contentText = contentMarkdown
|
|
memo.tags = tags
|
|
memo.notebookId = notebookId
|
|
memo.updatedAt = now
|
|
memo.excerpt = String(contentMarkdown.prefix(160))
|
|
if let contentJSON, let json = try? JSONValue.parse(contentJSON) {
|
|
memo.contentJson = json
|
|
}
|
|
try mirror.upsertMemo(scope: scope, memo: memo)
|
|
|
|
try outbox.enqueueUpdate(
|
|
scope: scope,
|
|
payload: MemoUpdatePayload(
|
|
memoId: memo.id,
|
|
expectedRevision: rev,
|
|
expectedContentHash: hash,
|
|
title: displayTitle,
|
|
contentMarkdown: contentMarkdown,
|
|
contentJson: contentJSON,
|
|
notebookId: notebookId,
|
|
tags: tags
|
|
)
|
|
)
|
|
try drafts.clear(scope: scope, key: DraftRepository.newKey)
|
|
try? drafts.clear(scope: scope, key: DraftRepository.memoKey(serverId))
|
|
return .updatedMaterialized(memoId: serverId)
|
|
}
|
|
|
|
let localId = "local:\(UUID().uuidString.lowercased())"
|
|
var placeholder = MemoDetail.localPlaceholder(
|
|
id: localId,
|
|
notebookId: notebookId,
|
|
title: title,
|
|
contentMarkdown: contentMarkdown,
|
|
tags: tags,
|
|
createdAt: now
|
|
)
|
|
// Persist TipTap JSON so image width attrs survive into detail/viewer.
|
|
if let contentJSON, let json = try? JSONValue.parse(contentJSON) {
|
|
placeholder.contentJson = json
|
|
}
|
|
try mirror.upsertMemo(scope: scope, memo: placeholder)
|
|
try outbox.enqueueCreate(
|
|
scope: scope,
|
|
payload: MemoCreatePayload(
|
|
memoId: localId,
|
|
title: displayTitle,
|
|
contentMarkdown: contentMarkdown,
|
|
contentJson: contentJSON,
|
|
notebookId: notebookId,
|
|
tags: tags,
|
|
createdAt: now
|
|
)
|
|
)
|
|
try drafts.clear(scope: scope, key: DraftRepository.newKey)
|
|
return .createdLocal(memoId: localId)
|
|
}
|
|
}
|