Replace the four per-mutation notification types (+add/+update/
+remove/+reorder) with a single +op envelope carrying {op_id,
kind, ...}. Pending mutations persist to a `notebook_outbox`
table on the client and replay on every .joined transition, so
an offline user's work propagates as soon as they're back on the
bus. Projects that never enable collaboration keep a zero-row
outbox — we only touch it once `collaboration_state.lab_id` is
set, and bulk-enqueue existing entries on createLab's success.
Model changes, in the same pass:
- NotebookEntry swaps `author: Author?` for `editors: [Author]`
(creator at index 0, each editor appended as the server applies
their update op). The boolean `isUserNote` is gone; callers now
read `entry.kind == .note` directly.
- Entries carry a `position: Double` — server-authoritative on
add (`max + 1000`), client-computed midpoint on drag. Clients
sort by `(position, id)` so concurrent reorders converge.
- Schema folds the new columns into v1 (no migration — Luma isn't
shipped).
UI:
- Mac: header renders a stack of editor avatars (tooltip = name,
tap opens the GitHub profile). Entry rows are draggable and
accept drops on top/bottom halves to compute the insertion slot.
- GTK: same avatar stack; reordering lives in the right-click
menu (Move Up / Move Down) since SwiftGtk's DragSource binding
isn't verified yet on this branch.
33 lines
1.0 KiB
Swift
33 lines
1.0 KiB
Swift
import Foundation
|
|
import GRDB
|
|
|
|
/// Row in `notebook_outbox`. Backs ProjectStore's pending-ops queue: each
|
|
/// row is a serialized NotebookOp awaiting delivery to the server.
|
|
struct NotebookOutboxRecord: Codable, Sendable, FetchableRecord, PersistableRecord {
|
|
static let databaseTableName = "notebook_outbox"
|
|
|
|
var opID: String
|
|
var kind: String
|
|
var entryID: String
|
|
var payloadJSON: String
|
|
var binaryData: Data?
|
|
var createdAt: Date
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case opID = "op_id"
|
|
case kind
|
|
case entryID = "entry_id"
|
|
case payloadJSON = "payload_json"
|
|
case binaryData = "binary_data"
|
|
case createdAt = "created_at"
|
|
}
|
|
|
|
func toOp() -> NotebookOp? {
|
|
guard let data = payloadJSON.data(using: .utf8),
|
|
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
else { return nil }
|
|
let bin = binaryData.map { Array<UInt8>($0) }
|
|
return NotebookOp.fromJSON(obj, binaryData: bin)
|
|
}
|
|
}
|