diff --git a/Cargo.toml b/Cargo.toml index 01c7023..ec11774 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ # Rust workspace. The framework-free client core, and the two shims that wrap it: # the Tauri desktop app and the uniffi bindings the native Android client loads. # Neither shim owns the core — that is the reason it is a crate at all rather than a -# module inside the desktop app (Scribe note 2730). +# module inside the desktop app. [workspace] resolver = "2" members = ["core", "desktop/src-tauri", "android/ffi", "android/bindgen"] diff --git a/core/Cargo.toml b/core/Cargo.toml index a39dfd8..a9bb38a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -9,15 +9,15 @@ edition = "2021" serde = { workspace = true } serde_json = { workspace = true } log = { workspace = true } -# Local-first store (M10.4): bundled = compile SQLite in, so there's no system +# Local-first store: bundled = compile SQLite in, so there's no system # libsqlite dependency to vary across the AppImage / native / Windows / Android builds. -# functions = the `fold` SQL function tag names are compared and indexed by (#5385): +# functions = the `fold` SQL function tag names are compared and indexed by: # SQLite's own lower() folds ASCII only, and the server folds every letter. rusqlite = { version = "0.32", features = ["bundled", "functions"] } uuid = { version = "1", features = ["v4"] } # RFC3339 timestamps for created_at/updated_at/remind_at (Date.parse-able on the JS side). chrono = { version = "0.4", default-features = false, features = ["clock"] } -# HTTP for the opt-in server handshake (M10.6) and the sync engine (M10.7). +# HTTP for the opt-in server handshake and the sync engine. # # native-tls, NOT rustls, deliberately: on x86_64-pc-windows-msvc native-tls resolves # to `schannel` — pure-Rust bindings to the OS TLS stack — so nothing C or assembly @@ -43,12 +43,11 @@ flate2 = { version = "1", default-features = false, features = ["rust_backend"] # Without this the build dies at `openssl-sys`: "Could not find directory of # OpenSSL installation". # -# `vendored` compiles OpenSSL from source with the NDK toolchain. The alternative -# was rustls on Android only, which builds faster — but rustls ships its own root -# store, so the phone would trust a DIFFERENT set of certificates than the desktop -# does. A self-hosted server behind a private or enterprise CA would then work on -# one surface and fail on another, and "the surfaces behave the same" is worth more -# than build minutes. +# `vendored` compiles OpenSSL from source with the NDK toolchain. Not rustls on +# Android, though it builds faster: rustls ships its own root store, so the phone +# would trust a DIFFERENT set of certificates than the desktop does, and a +# self-hosted server behind a private or enterprise CA would work on one surface +# and fail on another. # # Declared as a direct dependency purely to turn the feature on: cargo's feature # unification applies it to the copy `native-tls` pulls in transitively. diff --git a/core/src/lib.rs b/core/src/lib.rs index 919557b..f4e6519 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -2,10 +2,7 @@ //! //! Deliberately free of any UI framework. The desktop wraps it in Tauri commands; //! the Android client binds it through uniffi. Neither owns it, and a change to -//! either must not require touching this crate — that separation is the whole point -//! (see Scribe note 2730). It was already true before the split: every file here -//! carried zero Tauri references, which is what made the extraction a move rather -//! than a rewrite. +//! either must not require touching this crate — that separation is the whole point. //! //! - `local` — the source of truth. Works with no server and no account. //! - `sync` — entirely opt-in. Nothing in it runs until a server is linked. diff --git a/core/src/local/derive.rs b/core/src/local/derive.rs index 2a40f2e..a907279 100644 --- a/core/src/local/derive.rs +++ b/core/src/local/derive.rs @@ -6,7 +6,7 @@ //! letters, digits, `_` and `-`. On save these become labels attached with //! `via_tag = true`. The server and the web run the same cases //! (core/testdata/grammar.json). -//! - `- [ ] item`: a checklist item. The body IS the checklist (M304) — there is no +//! - `- [ ] item`: a checklist item. The body IS the checklist — there is no //! table of items beside it, so a list can sit between two paragraphs instead of //! only after them. //! @@ -17,9 +17,6 @@ //! and the rewriters below are how one is edited. //! //! Dedupes case-insensitively, preserving first-seen order. -//! -//! Also derived `[[wiki-links]]` until they were removed (note 2897) — this is a -//! capture-and-recall surface, and a linking system is organization. /// Every `#tag` in ONE line, as `(start, end, name)` in char indices. /// @@ -31,10 +28,9 @@ fn line_tags(chars: &[char]) -> Vec<(usize, usize, String)> { let mut i = 0; while i < chars.len() { if chars[i] == '#' { - // Start of line or after whitespace, and nothing else. Any non-tag - // character used to count, which made `(#todo)` a tag here and plain - // text on the server, and made the `/#section` of a pasted URL a label. - // Whitespace is the rule all three implementations now share (#5166). + // Start of line or after whitespace, and nothing else, so `(#todo)` and a + // pasted URL's `/#section` are not tags. All three implementations share + // this rule. let boundary = i == 0 || chars[i - 1].is_whitespace(); // A tag must start with a letter (so "#1" or a bare "#" is not a tag). if boundary && i + 1 < chars.len() && chars[i + 1].is_alphabetic() { @@ -54,8 +50,8 @@ fn line_tags(chars: &[char]) -> Vec<(usize, usize, String)> { /// One `#tag` and exactly where it sits, for a renderer drawing the body itself. /// -/// The card no longer prints a chip for a tag whose text is still in the note — it -/// colours the token where it was typed instead. To do that a renderer needs the +/// The card colours a tag where it was typed rather than repeating it as a chip. +/// To do that a renderer needs the /// SPAN, not just the name, and asking it to find the name again would be a second /// grammar quietly disagreeing with this one about what `##a` or `#1` is. #[derive(Debug, Clone, PartialEq, Eq)] @@ -142,8 +138,8 @@ fn collapse_blank_runs(text: &str) -> String { /// else is left exactly as written. /// /// The MIRROR of `split_body_tags` in the server's `notes/tags.py`, and it has to stay -/// one: a note lifted differently here than there would change under the operator the -/// moment it synced. Same discipline, and the same reason, as `DerivedTint`. +/// one: a note lifted differently here than there would change the moment it synced. +/// Same discipline, and the same reason, as `DerivedTint`. /// /// The conservative reading of "standalone" is deliberate. A trailing tag is /// ambiguous and the text does not say which it is — `buy milk #grocery` is filing, @@ -152,8 +148,8 @@ fn collapse_blank_runs(text: &str) -> String { /// /// `standalone` tags become ORDINARY labels (`via_tag = 0`): nothing is left to derive /// them from, so the row becomes the record and the chip's × becomes the way to remove -/// one. `inline` tags stay derived exactly as before. That is what `via_tag` means from -/// here on — backed by text still in the body. +/// one. `inline` tags stay derived. That is what `via_tag` means: backed by text still +/// in the body. pub fn lift_standalone_tags(body: &str) -> (Vec, Vec, String) { let mut standalone: Vec = Vec::new(); let mut inline: Vec = Vec::new(); @@ -220,7 +216,7 @@ fn is_tag_char(c: char) -> bool { /// How two tag names compare: equal once every letter is lowercased, accented ones /// included, so "Café" and "CAFÉ" are one tag. The server's `labeling.named` folds /// the same way (Postgres `lower`, Python `str.lower`), and the store's `fold` SQL -/// function is this (#5385). +/// function is this. pub(crate) fn fold(name: &str) -> String { name.to_lowercase() } @@ -248,8 +244,7 @@ fn push_unique(out: &mut Vec, candidate: &str) { // parse it would make a half-typed list stop being a list. /// A checklist item, as found in the body. Its position in the returned vector is -/// its identity — the same thing `position` meant when these were rows, and all the -/// wire ever carried (`push.rs` sent text and checked, never an id). +/// its identity; the wire carries text and checked, never an id. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DerivedItem { pub text: String, @@ -361,9 +356,8 @@ pub const DISPLAY_TITLE_CAP: usize = 200; /// twice, and they have to agree or a synced note is called different things on either /// side of the wire. Both run the `display_titles` cases in testdata/grammar.json. /// -/// It no longer needs the items, because the items ARE lines of the body now (M304). -/// What it needs instead is to strip the task marker off: a list-only note is still -/// named by its first item, and calling that note "- [ ] milk" would be showing +/// The items ARE lines of the body, so it strips the task marker off: a list-only +/// note is named by its first item, and calling that note "- [ ] milk" would be showing /// someone the storage rather than the note. An empty item is skipped rather than /// naming the note "", which is what a half-typed list would otherwise do. pub fn display_title(body: &str) -> String { @@ -431,9 +425,8 @@ pub fn set_item_checked(body: &str, index: usize, checked: bool) -> String { /// /// Spaced exactly as `import_export.py:_note_markdown` writes a list — a blank line /// between prose and the list, and nothing between consecutive items. That is not -/// cosmetic: the server migration folds existing rows into bodies using the same -/// layout, so an export taken before the migration and one taken after have to -/// agree byte for byte. +/// cosmetic: the migrations that fold checklist rows into bodies use the same layout, +/// so every surface lands on byte-identical bodies. /// /// `checked` is a parameter rather than always false because the two migrations that /// fold existing rows into bodies have to carry the state those rows were in. A new @@ -513,8 +506,8 @@ mod tests { // ── lifting standalone tags ────────────────────────────────────────────── // // The MIRROR of `split_body_tags` in the server's notes/tags.py, case for case. - // A note lifted differently here than there would change under the operator the - // moment it synced, so these are the cases that file agrees to. + // A note lifted differently here than there would change the moment it synced, + // so these are the cases that file agrees to. #[test] fn lifts_a_line_that_is_nothing_but_tags() { @@ -662,8 +655,8 @@ mod tests { #[test] fn append_spaces_like_the_exporter() { // Prose then a blank line then the list — byte-for-byte what - // import_export.py:_note_markdown writes, which is what the server - // migration will fold existing rows into. + // import_export.py:_note_markdown writes, which is the layout the server + // migration folds rows into. assert_eq!(append_item("a note", "milk", false), "a note\n\n- [ ] milk"); // Nothing between consecutive items. let one = "a note\n\n- [ ] milk"; diff --git a/core/src/local/mod.rs b/core/src/local/mod.rs index 87f04eb..7864def 100644 --- a/core/src/local/mod.rs +++ b/core/src/local/mod.rs @@ -73,9 +73,7 @@ pub fn open(path: &Path) -> rusqlite::Result { /// Open a migrated, in-memory store. /// /// Exists so a CONSUMER can test against a real schema without taking a rusqlite -/// dependency of its own just to build a `Db` — which is exactly what the desktop -/// crate was doing before the core was extracted. The Android bindings will want the -/// same thing. +/// dependency of its own just to build a `Db`. pub fn open_in_memory() -> rusqlite::Result { Ok(Db(Mutex::new(memory_conn()?))) } diff --git a/core/src/local/models.rs b/core/src/local/models.rs index dd42400..e94d8d1 100644 --- a/core/src/local/models.rs +++ b/core/src/local/models.rs @@ -45,7 +45,7 @@ pub struct Note { pub previews: Vec, pub created_at: Option, pub updated_at: Option, - /// How this account holds the note (#5175): `owner`, or `edit`/`view` for one + /// How this account holds the note: `owner`, or `edit`/`view` for one /// someone shared with it. Named and valued as the server's, so the shared /// frontend gates the editor identically either way. pub permission: String, @@ -119,7 +119,7 @@ pub struct Label { #[serde(skip_serializing_if = "Option::is_none")] pub count: Option, /// The newest `updated_at` among the untrashed notes carrying it, or None when - /// nothing does. The drawer keeps the few most recent tags in reach (#5931). + /// nothing does. The drawer keeps the few most recent tags in reach. pub last_used: Option, } diff --git a/core/src/local/retention.rs b/core/src/local/retention.rs index 9dd979e..b5b3a2c 100644 --- a/core/src/local/retention.rs +++ b/core/src/local/retention.rs @@ -1,4 +1,4 @@ -//! Trash retention for a device with no server (M11.3). +//! Trash retention for a device with no server. //! //! The server owns this policy whenever there IS one: a linked client learns about //! every permanent deletion from the delta feed, as a tombstone, and does exactly @@ -123,8 +123,7 @@ mod tests { fn a_note_just_inside_the_window_survives() { // The comparison is STRICTLY older than the cutoff, so a note with a minute // of its 30 days still to run is kept. An exact tie isn't testable against a - // wall clock — the sweep reads `now` microseconds after the row is stamped, - // which is precisely how the first version of this test failed. + // wall clock — the sweep reads `now` microseconds after the row is stamped. let conn = db(); let almost = Duration::days(30) - Duration::minutes(1); trashed_note_aged(&conn, "boundary", almost); diff --git a/core/src/local/schema.rs b/core/src/local/schema.rs index 9dc2990..3bc64f1 100644 --- a/core/src/local/schema.rs +++ b/core/src/local/schema.rs @@ -112,13 +112,11 @@ CREATE TABLE sync_state ( INSERT INTO sync_state (id) VALUES (1); "#; -// v2 (M10.7c): local tombstones. +// v2: local tombstones. // -// A permanent delete previously just dropped the row, which left NO record that it -// ever existed. Offline, that means the delete can never be pushed — and the next -// pull would faithfully resurrect the note from the server. A deletion that undoes -// itself is about the worst outcome sync can produce, so deletes are now recorded -// here until they've been acknowledged by the server and cleared. +// A permanent delete is recorded here until the server acknowledges it. Dropping the +// row alone leaves no record that it existed, so an offline delete could never be +// pushed and the next pull would resurrect the note. const SCHEMA_V2: &str = r#" CREATE TABLE pending_deletes ( entity TEXT NOT NULL, -- 'note' | 'label' @@ -128,7 +126,7 @@ CREATE TABLE pending_deletes ( ); "#; -// v3 (M10.7e): when the last successful sync finished. +// v3: when the last successful sync finished. // // The cursor alone can't answer "is this up to date?" — it's a revision watermark, // not a time, and it doesn't move at all when a sync legitimately finds nothing new. @@ -137,24 +135,21 @@ const SCHEMA_V3: &str = r#" ALTER TABLE sync_state ADD COLUMN last_sync_at TEXT; "#; -// v4 (M11.3): WHEN a note was trashed. +// v4: WHEN a note was trashed. // -// The table only ever recorded THAT a note was trashed, which is enough to draw a -// Trash view and nothing else. Retention needs an age: without a timestamp there is -// no way to tell a note trashed this morning from one trashed last spring, so an -// offline device could never expire its own trash — and the UI couldn't warn anyone -// before it did. +// Retention needs an age: without a timestamp there is no telling a note trashed this +// morning from one trashed last spring, so an offline device could never expire its +// own trash, and the UI couldn't warn anyone before it did. // It also records the LINKED server's retention window, captured from /api/config. -// Once linked, the server's policy is the one that actually applies, so showing this -// device's offline default would put a countdown on screen that doesn't match what -// happens — a wrong deadline is worse than none. +// Once linked, the server's policy is the one that applies, so showing this device's +// offline default would put a countdown on screen that doesn't match what happens. const SCHEMA_V4: &str = r#" ALTER TABLE notes ADD COLUMN trashed_at TEXT; UPDATE notes SET trashed_at = updated_at WHERE trashed = 1; ALTER TABLE sync_state ADD COLUMN server_retention_days INTEGER; "#; -// v5 (M10.9): small key/value app preferences. +// v5: small key/value app preferences. // // The first entry is the update channel, which is neither note data nor part of the // server link — so it belongs in neither `notes` nor `sync_state`. Generic on @@ -166,17 +161,16 @@ CREATE TABLE prefs ( ); "#; -// v6 (M13 step 2): `kind` is gone. A checklist is something a note HAS, not something -// a note IS — the column was a mode flag with no enum and no constraint behind it, -// and `note_items` was never tied to it. Dropping it loses nothing: a note that was -// 'list' keeps every one of its items. +// v6: drops `kind`. A checklist is something a note HAS, not something a note IS — +// the column was a mode flag with no enum and no constraint behind it. A note that +// was 'list' keeps every one of its items. // // SQLite has supported DROP COLUMN since 3.35 (2021); rusqlite bundles well past it. const SCHEMA_V6: &str = r#" ALTER TABLE notes DROP COLUMN kind; "#; -// v7 (M13 step 3): the title field is gone. A note is a body plus optional items, and +// v7: drops the title field. A note is a body plus optional items, and // its NAME is the first non-empty line of that body, falling back to its first item — // derived at read time, never stored (see store::display_title). // @@ -186,7 +180,7 @@ ALTER TABLE notes DROP COLUMN title; ALTER TABLE note_revisions DROP COLUMN title; "#; -// v8 (M304): `checklist_items` is gone. The body IS the checklist — a `- [ ] milk` +// v8: folds `checklist_items` into bodies. The body IS the checklist — a `- [ ] milk` // line is the item — so a list can sit between two paragraphs instead of only after // them, which a side table could never express no matter how it was styled. // @@ -251,13 +245,12 @@ fn migrate_v8(conn: &Connection) -> rusqlite::Result<()> { Ok(()) } -// v9 (M315): `notes.color` is gone. A card is one neutral surface now and colour lives -// only on a tag, so the column was written by a picker nothing read and read by nothing -// at all. `labels.color` is untouched — that is the colour that survived. +// v9: drops `notes.color`. A card is one neutral surface and colour lives only on a +// tag; `labels.color` is untouched. // // The saved-filter sweep is the second half and not optional. `params` is opaque JSON // and a stored view could carry `"color": "teal"`; with the facet gone that key would -// sit there forever, and a view that silently filters on a field the app no longer has +// sit there forever, and a view that silently filters on a field the app doesn't have // is worse than one that visibly lost a criterion. Guarded on `json_valid` because a // corrupt blob must keep whatever it holds, not become NULL. // @@ -275,12 +268,11 @@ UPDATE saved_filters AND params LIKE '%"color"%'; "#; -// v10 (#5168): an attachment can be created on THIS device. +// v10: an attachment can be created on THIS device. // -// Until now every attachment row arrived on the delta feed, so every one was already on -// the server. A file attached here, offline, is not, and `uploaded = 0` is the queue -// push drains once the note has landed. The rows already here all came from the -// server, which is why the default is 1. +// A file attached here, offline, is not on the server yet, and `uploaded = 0` is the +// queue push drains once the note has landed. Rows from before this step all came +// from the server, which is why the default is 1. // // `upload_error` holds a refusal that retrying won't fix — over the size limit, an id // already in use, bytes that don't match their hash. A row carrying one leaves the @@ -292,15 +284,15 @@ ALTER TABLE attachments ADD COLUMN uploaded INTEGER NOT NULL DEFAULT 1; ALTER TABLE attachments ADD COLUMN upload_error TEXT; "#; -// v11 (#5175): notes other people shared with this account. +// v11: notes other people shared with this account. // -// The feed now carries them, so a note says how it is held: `permission` is `owner`, +// The feed carries them, so a note says how it is held: `permission` is `owner`, // `edit` (its text may change here) or `view`. Every note already on a device is the // account's own, which is why the default is `owner`. `shared` is whether the owner // has shared it with anyone, and `shared_by_*` names the owner of one shared with us. // // `shares_synced` is whether this device has pulled with shares since it was linked. -// The notes shared before this build sit below the cursor it already holds, so the +// Notes shared before a device reaches this step sit below the cursor it holds, so the // first pull from a server offering `shares` starts again from zero (see // `engine::run_cycle`). A pull applies idempotently, so starting over costs a // download and nothing else. @@ -312,29 +304,29 @@ ALTER TABLE notes ADD COLUMN shared_by_name TEXT; ALTER TABLE sync_state ADD COLUMN shares_synced INTEGER NOT NULL DEFAULT 0; "#; -// v12 (#5176): a shared note's pin, archive and position are this account's own. +// v12: a shared note's pin, archive and position are this account's own. // // `state_at` is when they last changed here, on a note someone else owns. It is kept // apart from `updated_at`, which stays the time of the note's TEXT: pinning a copy // whose text is behind the owner's must not make that text look like the newer // edit when it is pushed. Null on our own notes, where `updated_at` covers both. // -// `sync_state.shares_synced` now holds a level rather than a flag (see +// `sync_state.shares_synced` holds a level rather than a flag (see // `state::SHARED_STATE`), and a device reaching this level pulls everything once // more: the shared notes it holds carry their owner's pins until it does. const SCHEMA_V12: &str = r#" ALTER TABLE notes ADD COLUMN state_at TEXT; "#; -// v13 (#5180): saved views are gone. They never synced, so this device's set was its +// v13: drops saved views. They never synced, so this device's set was its // own and nothing else holds a copy. const SCHEMA_V13: &str = r#" DROP TABLE saved_filters; "#; -// v14 (#5385): tag names compare by `fold`, which lowercases every letter. Until now -// the index and the queries used SQLite's lower(), which folds ASCII only, so "Café" -// and "CAFÉ" could be two tags on a device while the server held one. +// v14: tag names compare by `fold`, which lowercases every letter. SQLite's lower() +// folds ASCII only, so with it "Café" and "CAFÉ" could be two tags on a device while +// the server held one. // // Pairs that already exist are merged before the index can be rebuilt. THE OLDER ROW // SURVIVES, as in `store::rename_label`, so every device holding the same pair picks @@ -452,7 +444,7 @@ fn register_fold(conn: &Connection) -> rusqlite::Result<()> { mod tests { use super::*; - /// A database as it stood before M304 — items still in their own table. + /// A database at v7, with items still in their own table. fn v7_db() -> Connection { let conn = Connection::open_in_memory().expect("open"); conn.execute_batch("PRAGMA foreign_keys = ON;").expect("fk"); @@ -602,8 +594,7 @@ mod tests { .collect::>>() .expect("collect"); assert!(!columns.iter().any(|c| c == "color")); - // The one that survived. Getting this wrong would take every tag's colour with - // it, which is the whole thing M315 was keeping. + // The one that stays. Getting this wrong would take every tag's colour with it. let mut stmt = conn.prepare("PRAGMA table_info(labels)").expect("pragma"); let label_columns: Vec = stmt .query_map([], |r| r.get::<_, String>(1)) diff --git a/core/src/local/store.rs b/core/src/local/store.rs index 153dc0f..40a9239 100644 --- a/core/src/local/store.rs +++ b/core/src/local/store.rs @@ -48,11 +48,9 @@ fn load_labels(conn: &Connection, note_id: &str) -> rusqlite::Result Vec { derive::extract_items(body) .into_iter() @@ -122,10 +120,9 @@ fn load_note(conn: &Connection, id: &str) -> rusqlite::Result { permission, shared, shared_by_id, shared_by_name FROM notes WHERE id = ?1", [id], - // By NAME, not by position. Dropping `color` shifted every column after it - // and the two timestamps were missed (fa89da1): created_at read updated_at - // and updated_at read trashed_at, with nothing failing. A name either - // resolves or errors. + // By NAME, not by position: dropping a column shifts every one after it, and + // a positional read would then load the wrong timestamps with nothing + // failing. A name either resolves or errors. |r| { Ok(Note { id: r.get("id")?, @@ -166,11 +163,11 @@ fn load_note(conn: &Connection, id: &str) -> rusqlite::Result { Ok(note) } -// ---- notes shared with this account (#5175) --------------------------------- +// ---- notes shared with this account ------------------------------------------ // // A note someone else owns arrives with `permission` `edit` (its text may change // here) or `view` (it may not). Its pin, archive and place on this board are this -// account's own either way (#5176). Everything else about it — trash, reminders, +// account's own either way. Everything else about it — trash, reminders, // labels, files — stays its owner's. The server enforces the same split; // refusing here as well tells the person at once, instead of letting an edit be made, // queued, and then thrown away by the next sync. @@ -268,21 +265,19 @@ fn find_or_create_label(conn: &Connection, name: &str) -> rusqlite::Result rusqlite::Result<()> { @@ -384,9 +379,8 @@ pub fn list_notes(conn: &Connection, q: &ListQuery) -> rusqlite::Result rusqlite::Result } /// The longest snooze, in minutes: 30 days. The server's snooze route clamps to the -/// same range (`notes.SNOOZE_MAX_MINUTES`), so a device and the web agree (#5386). +/// same range (`notes.SNOOZE_MAX_MINUTES`), so a device and the web agree. pub const SNOOZE_MAX_MINUTES: i64 = 60 * 24 * 30; pub fn snooze_reminder(conn: &Connection, id: &str, minutes: i64) -> rusqlite::Result { @@ -870,7 +864,7 @@ pub fn reorder(conn: &Connection, ordered_ids: &[String]) -> rusqlite::Result<() let total = ordered_ids.len() as i64; let at = now(); for (i, id) in ordered_ids.iter().enumerate() { - // A note someone shared with us moves on this account's board only (#5176), + // A note someone shared with us moves on this account's board only, // stamped as its own state rather than as an edit. conn.execute( "UPDATE notes @@ -1024,11 +1018,10 @@ pub fn create_label(conn: &Connection, name: &str) -> rusqlite::Result