Engine: deleteAddressNoteMessage drops the row from the store and enqueues an address_note_message_remove op. Collab apply mirrors the existing edit path. AddressNoteChange grows a messageRemoved case. GTK: right-click on a user message offers Edit and Delete. Edit swaps the body label for an inline editor; Save commits via editUserMessage. Delete confirms first, then removes the row from the popover and via the engine.
187 lines
6.1 KiB
Swift
187 lines
6.1 KiB
Swift
import Foundation
|
|
|
|
public enum AddressNoteOp: Sendable {
|
|
case noteUpsert(NoteUpsert)
|
|
case noteRemove(NoteRemove)
|
|
case messageAppend(MessageAppend)
|
|
case messageEdit(MessageEdit)
|
|
case messageRemove(MessageRemove)
|
|
|
|
public var opID: UUID {
|
|
switch self {
|
|
case .noteUpsert(let u): return u.opID
|
|
case .noteRemove(let r): return r.opID
|
|
case .messageAppend(let a): return a.opID
|
|
case .messageEdit(let e): return e.opID
|
|
case .messageRemove(let r): return r.opID
|
|
}
|
|
}
|
|
|
|
public var noteID: UUID {
|
|
switch self {
|
|
case .noteUpsert(let u): return u.note.id
|
|
case .noteRemove(let r): return r.noteID
|
|
case .messageAppend(let a): return a.message.noteID
|
|
case .messageEdit(let e): return e.noteID
|
|
case .messageRemove(let r): return r.noteID
|
|
}
|
|
}
|
|
|
|
public var kind: String {
|
|
switch self {
|
|
case .noteUpsert: return "address_note_upsert"
|
|
case .noteRemove: return "address_note_remove"
|
|
case .messageAppend: return "address_note_message_append"
|
|
case .messageEdit: return "address_note_message_edit"
|
|
case .messageRemove: return "address_note_message_remove"
|
|
}
|
|
}
|
|
|
|
public struct NoteUpsert: Sendable {
|
|
public let opID: UUID
|
|
public var note: AddressNote
|
|
|
|
public init(opID: UUID = UUID(), note: AddressNote) {
|
|
self.opID = opID
|
|
self.note = note
|
|
}
|
|
}
|
|
|
|
public struct NoteRemove: Sendable {
|
|
public let opID: UUID
|
|
public let noteID: UUID
|
|
|
|
public init(opID: UUID = UUID(), noteID: UUID) {
|
|
self.opID = opID
|
|
self.noteID = noteID
|
|
}
|
|
}
|
|
|
|
public struct MessageAppend: Sendable {
|
|
public let opID: UUID
|
|
public let message: AddressNoteMessage
|
|
|
|
public init(opID: UUID = UUID(), message: AddressNoteMessage) {
|
|
self.opID = opID
|
|
self.message = message
|
|
}
|
|
}
|
|
|
|
public struct MessageEdit: Sendable {
|
|
public let opID: UUID
|
|
public let noteID: UUID
|
|
public let messageID: UUID
|
|
public let bodyMarkdown: String
|
|
|
|
public init(opID: UUID = UUID(), noteID: UUID, messageID: UUID, bodyMarkdown: String) {
|
|
self.opID = opID
|
|
self.noteID = noteID
|
|
self.messageID = messageID
|
|
self.bodyMarkdown = bodyMarkdown
|
|
}
|
|
}
|
|
|
|
public struct MessageRemove: Sendable {
|
|
public let opID: UUID
|
|
public let noteID: UUID
|
|
public let messageID: UUID
|
|
|
|
public init(opID: UUID = UUID(), noteID: UUID, messageID: UUID) {
|
|
self.opID = opID
|
|
self.noteID = noteID
|
|
self.messageID = messageID
|
|
}
|
|
}
|
|
|
|
public func toJSON() -> [String: Any] {
|
|
var obj: [String: Any] = [
|
|
"op_id": opID.uuidString,
|
|
"kind": kind,
|
|
]
|
|
switch self {
|
|
case .noteUpsert(let u):
|
|
obj["note"] = encodeRow(u.note)
|
|
case .noteRemove(let r):
|
|
obj["note_id"] = r.noteID.uuidString
|
|
case .messageAppend(let a):
|
|
obj["message"] = encodeRow(a.message)
|
|
case .messageEdit(let e):
|
|
obj["note_id"] = e.noteID.uuidString
|
|
obj["message_id"] = e.messageID.uuidString
|
|
obj["body_markdown"] = e.bodyMarkdown
|
|
case .messageRemove(let r):
|
|
obj["note_id"] = r.noteID.uuidString
|
|
obj["message_id"] = r.messageID.uuidString
|
|
}
|
|
return obj
|
|
}
|
|
|
|
public static func fromJSON(_ obj: [String: Any]) -> AddressNoteOp? {
|
|
guard let opIDStr = obj["op_id"] as? String,
|
|
let opID = UUID(uuidString: opIDStr),
|
|
let kind = obj["kind"] as? String
|
|
else { return nil }
|
|
|
|
switch kind {
|
|
case "address_note_upsert":
|
|
guard let row = obj["note"] as? [String: Any],
|
|
let note: AddressNote = decodeRow(row)
|
|
else { return nil }
|
|
return .noteUpsert(NoteUpsert(opID: opID, note: note))
|
|
case "address_note_remove":
|
|
guard let idStr = obj["note_id"] as? String,
|
|
let id = UUID(uuidString: idStr)
|
|
else { return nil }
|
|
return .noteRemove(NoteRemove(opID: opID, noteID: id))
|
|
case "address_note_message_append":
|
|
guard let row = obj["message"] as? [String: Any],
|
|
let message: AddressNoteMessage = decodeRow(row)
|
|
else { return nil }
|
|
return .messageAppend(MessageAppend(opID: opID, message: message))
|
|
case "address_note_message_edit":
|
|
guard let nIDStr = obj["note_id"] as? String,
|
|
let nID = UUID(uuidString: nIDStr),
|
|
let mIDStr = obj["message_id"] as? String,
|
|
let mID = UUID(uuidString: mIDStr),
|
|
let body = obj["body_markdown"] as? String
|
|
else { return nil }
|
|
return .messageEdit(MessageEdit(opID: opID, noteID: nID, messageID: mID, bodyMarkdown: body))
|
|
case "address_note_message_remove":
|
|
guard let nIDStr = obj["note_id"] as? String,
|
|
let nID = UUID(uuidString: nIDStr),
|
|
let mIDStr = obj["message_id"] as? String,
|
|
let mID = UUID(uuidString: mIDStr)
|
|
else { return nil }
|
|
return .messageRemove(MessageRemove(opID: opID, noteID: nID, messageID: mID))
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private let wireEncoder: JSONEncoder = {
|
|
let e = JSONEncoder()
|
|
e.dateEncodingStrategy = .iso8601
|
|
return e
|
|
}()
|
|
|
|
private let wireDecoder: JSONDecoder = {
|
|
let d = JSONDecoder()
|
|
d.dateDecodingStrategy = .iso8601
|
|
return d
|
|
}()
|
|
|
|
private func encodeRow<T: Encodable>(_ value: T) -> [String: Any] {
|
|
guard let data = try? wireEncoder.encode(value),
|
|
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
else { return [:] }
|
|
return obj
|
|
}
|
|
|
|
private func decodeRow<T: Decodable>(_ obj: [String: Any]) -> T? {
|
|
guard let data = try? JSONSerialization.data(withJSONObject: obj),
|
|
let value = try? wireDecoder.decode(T.self, from: data)
|
|
else { return nil }
|
|
return value
|
|
}
|