commit 09eeb57bd918e34e9539d6b823e318f8be43892c
parent a8c6baeacb8247f1f2eb5f6690efdcdafde90624
Author: William Casarin <jb55@jb55.com>
Date: Thu, 31 Jul 2025 16:05:43 -0700
Merge notebook (feature gated)
This merges the experimental notebook app, which can be
enabled with --notebook.
We also switch to bitflags for notedeck options
Diffstat:
19 files changed, 812 insertions(+), 63 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1008,6 +1008,7 @@ dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
+ "serde",
"wasm-bindgen",
"windows-link",
]
@@ -1263,6 +1264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
+ "serde",
]
[[package]]
@@ -1409,6 +1411,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
name = "ecolor"
version = "0.31.1"
source = "git+https://github.com/damus-io/egui?rev=a67ab901e197ce13948ff7d00aa6e07e31a68ccd#a67ab901e197ce13948ff7d00aa6e07e31a68ccd"
@@ -1626,7 +1634,7 @@ version = "0.3.0"
dependencies = [
"bech32",
"ewebsock",
- "hashbrown",
+ "hashbrown 0.15.4",
"hex",
"mio",
"nostr 0.37.0",
@@ -2300,7 +2308,7 @@ checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca"
dependencies = [
"bitflags 2.9.1",
"gpu-descriptor-types",
- "hashbrown",
+ "hashbrown 0.15.4",
]
[[package]]
@@ -2324,6 +2332,12 @@ dependencies = [
[[package]]
name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
@@ -2367,6 +2381,17 @@ dependencies = [
]
[[package]]
+name = "hex_color"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d37f101bf4c633f7ca2e4b5e136050314503dd198e78e325ea602c327c484ef0"
+dependencies = [
+ "arrayvec",
+ "rand 0.8.5",
+ "serde",
+]
+
+[[package]]
name = "hex_lit"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2697,12 +2722,23 @@ checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
[[package]]
name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
- "hashbrown",
+ "hashbrown 0.15.4",
"serde",
]
@@ -2891,6 +2927,19 @@ dependencies = [
]
[[package]]
+name = "jsoncanvas"
+version = "0.1.6"
+source = "git+https://github.com/jb55/jsoncanvas?rev=ae60f96e4d022cf037e086b793cacc3225bc14e5#ae60f96e4d022cf037e086b793cacc3225bc14e5"
+dependencies = [
+ "hex_color",
+ "serde",
+ "serde_json",
+ "serde_with",
+ "thiserror 1.0.69",
+ "url",
+]
+
+[[package]]
name = "khronos-egl"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3212,7 +3261,7 @@ dependencies = [
"cfg_aliases",
"codespan-reporting",
"hexf-parse",
- "indexmap",
+ "indexmap 2.9.0",
"log",
"rustc-hash 1.1.0",
"spirv",
@@ -3434,6 +3483,7 @@ dependencies = [
"base32",
"bech32",
"bincode",
+ "bitflags 2.9.1",
"blurhash",
"dirs",
"eframe",
@@ -3445,7 +3495,7 @@ dependencies = [
"fluent",
"fluent-langneg",
"fluent-resmgr",
- "hashbrown",
+ "hashbrown 0.15.4",
"hex",
"image",
"jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -3490,6 +3540,7 @@ dependencies = [
"notedeck",
"notedeck_columns",
"notedeck_dave",
+ "notedeck_notebook",
"notedeck_ui",
"profiling",
"puffin",
@@ -3523,11 +3574,11 @@ dependencies = [
"egui_virtual_list",
"ehttp",
"enostr",
- "hashbrown",
+ "hashbrown 0.15.4",
"hex",
"human_format",
"image",
- "indexmap",
+ "indexmap 2.9.0",
"nostrdb",
"notedeck",
"notedeck_ui",
@@ -3585,6 +3636,15 @@ dependencies = [
]
[[package]]
+name = "notedeck_notebook"
+version = "0.5.9"
+dependencies = [
+ "egui",
+ "jsoncanvas",
+ "notedeck",
+]
+
+[[package]]
name = "notedeck_ui"
version = "0.5.9"
dependencies = [
@@ -3595,7 +3655,7 @@ dependencies = [
"egui_extras",
"ehttp",
"enostr",
- "hashbrown",
+ "hashbrown 0.15.4",
"image",
"nostrdb",
"notedeck",
@@ -4424,7 +4484,7 @@ source = "git+https://github.com/jb55/puffin?rev=c6a6242adaf90b6292c0f462d2acd34
dependencies = [
"egui",
"egui_extras",
- "indexmap",
+ "indexmap 2.9.0",
"natord",
"once_cell",
"parking_lot",
@@ -4753,6 +4813,26 @@ dependencies = [
]
[[package]]
+name = "ref-cast"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5114,6 +5194,30 @@ dependencies = [
]
[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5266,7 +5370,7 @@ version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
- "indexmap",
+ "indexmap 2.9.0",
"itoa",
"memchr",
"ryu",
@@ -5306,6 +5410,38 @@ dependencies = [
]
[[package]]
+name = "serde_with"
+version = "3.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
+dependencies = [
+ "base64 0.22.1",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.9.0",
+ "schemars 0.9.0",
+ "schemars 1.0.4",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5905,7 +6041,7 @@ version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
- "indexmap",
+ "indexmap 2.9.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -6688,7 +6824,7 @@ dependencies = [
"bitflags 2.9.1",
"cfg_aliases",
"document-features",
- "indexmap",
+ "indexmap 2.9.0",
"log",
"naga",
"once_cell",
diff --git a/Cargo.toml b/Cargo.toml
@@ -6,6 +6,7 @@ members = [
"crates/notedeck_chrome",
"crates/notedeck_columns",
"crates/notedeck_dave",
+ "crates/notedeck_notebook",
"crates/notedeck_ui",
"crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui",
@@ -48,6 +49,7 @@ notedeck = { path = "crates/notedeck" }
notedeck_chrome = { path = "crates/notedeck_chrome" }
notedeck_columns = { path = "crates/notedeck_columns" }
notedeck_dave = { path = "crates/notedeck_dave" }
+notedeck_notebook = { path = "crates/notedeck_notebook" }
notedeck_ui = { path = "crates/notedeck_ui" }
tokenator = { path = "crates/tokenator" }
once_cell = "1.19.0"
diff --git a/crates/notedeck/Cargo.toml b/crates/notedeck/Cargo.toml
@@ -47,6 +47,7 @@ fluent-langneg = { workspace = true }
unic-langid = { workspace = true }
once_cell = { workspace = true }
md5 = { workspace = true }
+bitflags = { workspace = true }
regex = "1"
[dev-dependencies]
diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs
@@ -4,6 +4,7 @@ use crate::persist::{AppSizeHandler, SettingsHandler};
use crate::wallet::GlobalWallet;
use crate::zaps::Zaps;
use crate::JobPool;
+use crate::NotedeckOptions;
use crate::{
frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath,
DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, UnknownIds,
@@ -109,7 +110,7 @@ impl eframe::App for Notedeck {
});
self.app_size.try_save_app_size(ctx);
- if self.args.relay_debug {
+ if self.args.options.contains(NotedeckOptions::RelayDebug) {
if self.pool.debug.is_none() {
self.pool.use_debug();
}
@@ -170,7 +171,7 @@ impl Notedeck {
let config = Config::new().set_ingester_threads(2).set_mapsize(map_size);
- let keystore = if parsed_args.use_keystore {
+ let keystore = if parsed_args.options.contains(NotedeckOptions::UseKeystore) {
let keys_path = path.path(DataPathType::Keys);
let selected_key_path = path.path(DataPathType::SelectedKey);
Some(AccountStorage::new(
@@ -276,6 +277,10 @@ impl Notedeck {
}
}
+ pub fn options(&self) -> NotedeckOptions {
+ self.args.options
+ }
+
pub fn app<A: App + 'static>(mut self, app: A) -> Self {
self.set_app(app);
self
diff --git a/crates/notedeck/src/args.rs b/crates/notedeck/src/args.rs
@@ -1,23 +1,15 @@
use std::collections::BTreeSet;
+use crate::NotedeckOptions;
use enostr::{Keypair, Pubkey, SecretKey};
use tracing::error;
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
pub struct Args {
pub relays: Vec<String>,
- pub is_mobile: Option<bool>,
pub locale: Option<LanguageIdentifier>,
- pub show_note_client: bool,
pub keys: Vec<Keypair>,
- pub light: bool,
- pub debug: bool,
- pub relay_debug: bool,
-
- /// Enable when running tests so we don't panic on app startup
- pub tests: bool,
-
- pub use_keystore: bool,
+ pub options: NotedeckOptions,
pub dbpath: Option<String>,
pub datapath: Option<String>,
}
@@ -28,14 +20,8 @@ impl Args {
let mut unrecognized_args = BTreeSet::new();
let mut res = Args {
relays: vec![],
- is_mobile: None,
keys: vec![],
- light: false,
- show_note_client: false,
- debug: false,
- relay_debug: false,
- tests: false,
- use_keystore: true,
+ options: NotedeckOptions::default(),
dbpath: None,
datapath: None,
locale: None,
@@ -47,9 +33,9 @@ impl Args {
let arg = &args[i];
if arg == "--mobile" {
- res.is_mobile = Some(true);
+ res.options.set(NotedeckOptions::Mobile, true);
} else if arg == "--light" {
- res.light = true;
+ res.options.set(NotedeckOptions::LightTheme, true);
} else if arg == "--locale" {
i += 1;
let Some(locale) = args.get(i) else {
@@ -68,11 +54,11 @@ impl Args {
}
}
} else if arg == "--dark" {
- res.light = false;
+ res.options.set(NotedeckOptions::LightTheme, false);
} else if arg == "--debug" {
- res.debug = true;
+ res.options.set(NotedeckOptions::Debug, true);
} else if arg == "--testrunner" {
- res.tests = true;
+ res.options.set(NotedeckOptions::Tests, true);
} else if arg == "--pub" || arg == "--npub" {
i += 1;
let pubstr = if let Some(next_arg) = args.get(i) {
@@ -135,11 +121,13 @@ impl Args {
};
res.relays.push(relay.clone());
} else if arg == "--no-keystore" {
- res.use_keystore = false;
+ res.options.set(NotedeckOptions::UseKeystore, true);
} else if arg == "--relay-debug" {
- res.relay_debug = true;
- } else if arg == "--show-note-client" {
- res.show_note_client = true;
+ res.options.set(NotedeckOptions::RelayDebug, true);
+ } else if arg == "--show-client" {
+ res.options.set(NotedeckOptions::ShowClient, true);
+ } else if arg == "--notebook" {
+ res.options.set(NotedeckOptions::FeatureNotebook, true);
} else {
unrecognized_args.insert(arg.clone());
}
diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs
@@ -18,6 +18,7 @@ mod muted;
pub mod name;
pub mod note;
mod notecache;
+mod options;
mod persist;
pub mod platform;
pub mod profile;
@@ -68,6 +69,7 @@ pub use note::{
RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction,
};
pub use notecache::{CachedNote, NoteCache};
+pub use options::NotedeckOptions;
pub use persist::*;
pub use profile::get_profile_url;
pub use relay_debug::RelayDebugView;
diff --git a/crates/notedeck/src/options.rs b/crates/notedeck/src/options.rs
@@ -0,0 +1,39 @@
+use bitflags::bitflags;
+
+bitflags! {
+ #[repr(transparent)]
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+ pub struct NotedeckOptions: u64 {
+ // ===== Settings ======
+ /// Are we on light theme?
+ const LightTheme = 1 << 0;
+
+ /// Debug controls, fps stats
+ const Debug = 1 << 1;
+
+ /// Show relay debug window?
+ const RelayDebug = 1 << 2;
+
+ /// Are we running as tests?
+ const Tests = 1 << 3;
+
+ /// Use keystore?
+ const UseKeystore = 1 << 4;
+
+ /// Show client on notes?
+ const ShowClient = 1 << 5;
+
+ /// Simulate is_compiled_as_mobile ?
+ const Mobile = 1 << 6;
+
+ // ===== Feature Flags ======
+ /// Is notebook enabled?
+ const FeatureNotebook = 1 << 32;
+ }
+}
+
+impl Default for NotedeckOptions {
+ fn default() -> Self {
+ NotedeckOptions::UseKeystore
+ }
+}
diff --git a/crates/notedeck_chrome/Cargo.toml b/crates/notedeck_chrome/Cargo.toml
@@ -16,6 +16,7 @@ egui = { workspace = true }
notedeck_columns = { workspace = true }
notedeck_ui = { workspace = true }
notedeck_dave = { workspace = true }
+notedeck_notebook = { workspace = true }
notedeck = { workspace = true }
nostrdb = { workspace = true }
puffin = { workspace = true, optional = true }
diff --git a/crates/notedeck_chrome/src/android.rs b/crates/notedeck_chrome/src/android.rs
@@ -5,6 +5,7 @@ use egui_winit::winit::platform::android::activity::AndroidApp;
use notedeck::enostr::Error;
use notedeck_columns::Damus;
use notedeck_dave::Dave;
+use notedeck_notebook::Notebook;
use crate::{app::NotedeckApp, chrome::Chrome, setup::setup_chrome};
use notedeck::Notedeck;
@@ -80,6 +81,7 @@ pub async fn android_main(app: AndroidApp) {
let context = &mut notedeck.app_context();
let dave = Dave::new(cc.wgpu_render_state.as_ref());
let columns = Damus::new(context, &app_args);
+ let notebook = Notebook::new();
let mut chrome = Chrome::new();
// ensure we recognized all the arguments
@@ -93,8 +95,15 @@ pub async fn android_main(app: AndroidApp) {
return Err(Error::Empty.into());
}
- chrome.add_app(NotedeckApp::Columns(columns));
- chrome.add_app(NotedeckApp::Dave(dave));
+ chrome.add_app(NotedeckApp::Columns(Box::new(columns)));
+ chrome.add_app(NotedeckApp::Dave(Box::new(dave)));
+
+ if notedeck
+ .options()
+ .contains(NotedeckOptions::FeaturesNotebook)
+ {
+ chrome.add_app(NotedeckApp::Notebook(Box::default()));
+ }
// test dav
chrome.set_active(0);
diff --git a/crates/notedeck_chrome/src/app.rs b/crates/notedeck_chrome/src/app.rs
@@ -1,11 +1,13 @@
use notedeck::{AppAction, AppContext};
use notedeck_columns::Damus;
use notedeck_dave::Dave;
+use notedeck_notebook::Notebook;
#[allow(clippy::large_enum_variant)]
pub enum NotedeckApp {
- Dave(Dave),
- Columns(Damus),
+ Dave(Box<Dave>),
+ Columns(Box<Damus>),
+ Notebook(Box<Notebook>),
Other(Box<dyn notedeck::App>),
}
@@ -14,6 +16,7 @@ impl notedeck::App for NotedeckApp {
match self {
NotedeckApp::Dave(dave) => dave.update(ctx, ui),
NotedeckApp::Columns(columns) => columns.update(ctx, ui),
+ NotedeckApp::Notebook(notebook) => notebook.update(ctx, ui),
NotedeckApp::Other(other) => other.update(ctx, ui),
}
}
diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs
@@ -6,12 +6,14 @@ use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference
use egui_extras::{Size, StripBuilder};
use nostrdb::{ProfileRecord, Transaction};
use notedeck::{
- tr, App, AppAction, AppContext, Localization, NotedeckTextStyle, UserAccount, WalletType,
+ tr, App, AppAction, AppContext, Localization, NotedeckOptions, NotedeckTextStyle, UserAccount,
+ WalletType,
};
use notedeck_columns::{
column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus,
};
use notedeck_dave::{Dave, DaveAvatar};
+use notedeck_notebook::Notebook;
use notedeck_ui::{app_images, AnimationHelper, ProfilePic};
static ICON_WIDTH: f32 = 40.0;
@@ -199,6 +201,16 @@ impl Chrome {
None
}
+ fn get_notebook(&mut self) -> Option<&mut Notebook> {
+ for app in &mut self.apps {
+ if let NotedeckApp::Notebook(notebook) = app {
+ return Some(notebook);
+ }
+ }
+
+ None
+ }
+
fn switch_to_dave(&mut self) {
for (i, app) in self.apps.iter().enumerate() {
if let NotedeckApp::Dave(_) = app {
@@ -207,6 +219,14 @@ impl Chrome {
}
}
+ fn switch_to_notebook(&mut self) {
+ for (i, app) in self.apps.iter().enumerate() {
+ if let NotedeckApp::Notebook(_) = app {
+ self.active = i as i32;
+ }
+ }
+ }
+
fn switch_to_columns(&mut self) {
for (i, app) in self.apps.iter().enumerate() {
if let NotedeckApp::Columns(_) = app {
@@ -428,13 +448,11 @@ impl Chrome {
ui.add(milestone_name(i18n));
ui.add_space(16.0);
//let dark_mode = ui.ctx().style().visuals.dark_mode;
+ if columns_button(ui)
+ .on_hover_cursor(egui::CursorIcon::PointingHand)
+ .clicked()
{
- if columns_button(ui)
- .on_hover_cursor(egui::CursorIcon::PointingHand)
- .clicked()
- {
- self.active = 0;
- }
+ self.active = 0;
}
ui.add_space(32.0);
@@ -446,6 +464,16 @@ impl Chrome {
self.switch_to_dave();
}
}
+ //ui.add_space(32.0);
+
+ if let Some(_notebook) = self.get_notebook() {
+ if notebook_button(ui)
+ .on_hover_cursor(egui::CursorIcon::PointingHand)
+ .clicked()
+ {
+ self.switch_to_notebook();
+ }
+ }
}
}
@@ -583,6 +611,16 @@ fn accounts_button(ui: &mut egui::Ui) -> egui::Response {
)
}
+fn notebook_button(ui: &mut egui::Ui) -> egui::Response {
+ expanding_button(
+ "notebook-button",
+ 40.0,
+ app_images::algo_image(),
+ app_images::algo_image(),
+ ui,
+ )
+}
+
fn dave_sidebar_rect(ui: &mut egui::Ui) -> Rect {
let size = vec2(60.0, 60.0);
let available = ui.available_rect_before_wrap();
@@ -861,7 +899,7 @@ fn bottomup_sidebar(
.add(wallet_button())
.on_hover_cursor(egui::CursorIcon::PointingHand);
- if ctx.args.debug {
+ if ctx.args.options.contains(NotedeckOptions::Debug) {
ui.weak(format!("{}", ctx.frame_history.fps() as i32));
ui.weak(format!(
"{:10.1}",
diff --git a/crates/notedeck_chrome/src/notedeck.rs b/crates/notedeck_chrome/src/notedeck.rs
@@ -10,7 +10,7 @@ static GLOBAL: AccountingAllocator<std::alloc::System> =
AccountingAllocator::new(std::alloc::System);
use notedeck::enostr::Error;
-use notedeck::{DataPath, DataPathType, Notedeck};
+use notedeck::{DataPath, DataPathType, Notedeck, NotedeckOptions};
use notedeck_chrome::{
setup::{generate_native_options, setup_chrome},
Chrome, NotedeckApp,
@@ -117,8 +117,15 @@ async fn main() {
return Err(Error::Empty.into());
}
- chrome.add_app(NotedeckApp::Columns(columns));
- chrome.add_app(NotedeckApp::Dave(dave));
+ chrome.add_app(NotedeckApp::Columns(Box::new(columns)));
+ chrome.add_app(NotedeckApp::Dave(Box::new(dave)));
+
+ if notedeck
+ .options()
+ .contains(NotedeckOptions::FeatureNotebook)
+ {
+ chrome.add_app(NotedeckApp::Notebook(Box::default()));
+ }
chrome.set_active(0);
diff --git a/crates/notedeck_chrome/src/setup.rs b/crates/notedeck_chrome/src/setup.rs
@@ -2,7 +2,7 @@ use crate::{fonts, theme};
use eframe::NativeOptions;
use egui::{FontId, ThemePreference};
-use notedeck::{AppSizeHandler, DataPath, NotedeckTextStyle};
+use notedeck::{AppSizeHandler, DataPath, NotedeckOptions, NotedeckTextStyle};
use notedeck_ui::app_images;
use tracing::info;
@@ -13,16 +13,20 @@ pub fn setup_chrome(
note_body_font_size: f32,
zoom_factor: f32,
) {
- let is_mobile = args
- .is_mobile
- .unwrap_or(notedeck::ui::is_compiled_as_mobile());
+ let is_mobile =
+ args.options.contains(NotedeckOptions::Mobile) || notedeck::ui::is_compiled_as_mobile();
let is_oled = notedeck::ui::is_oled();
// Some people have been running notedeck in debug, let's catch that!
- if !args.tests && cfg!(debug_assertions) && !args.debug {
+ if !args.options.contains(NotedeckOptions::Tests)
+ && cfg!(debug_assertions)
+ && !args.options.contains(NotedeckOptions::Debug)
+ {
println!("--- WELCOME TO DAMUS NOTEDECK! ---");
- println!("It looks like are running notedeck in debug mode, unless you are a developer, this is not likely what you want.");
+ println!(
+ "It looks like are running notedeck in debug mode, unless you are a developer, this is not likely what you want."
+ );
println!("If you are a developer, run `cargo run -- --debug` to skip this message.");
println!("For everyone else, try again with `cargo run --release`. Enjoy!");
println!("---------------------------------");
diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs
@@ -19,7 +19,7 @@ use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPo
use nostrdb::Transaction;
use notedeck::{
tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState,
- Images, JobsCache, Localization, SettingsHandler, UnknownIds,
+ Images, JobsCache, Localization, NotedeckOptions, SettingsHandler, UnknownIds,
};
use notedeck_ui::{
media::{MediaViewer, MediaViewerFlags, MediaViewerState},
@@ -442,7 +442,10 @@ impl Damus {
let mut options = AppOptions::default();
let tmp_columns = !parsed_args.columns.is_empty();
options.set(AppOptions::TmpColumns, tmp_columns);
- options.set(AppOptions::Debug, app_context.args.debug);
+ options.set(
+ AppOptions::Debug,
+ app_context.args.options.contains(NotedeckOptions::Debug),
+ );
options.set(
AppOptions::SinceOptimize,
parsed_args.is_flag_set(ColumnsFlag::SinceOptimize),
diff --git a/crates/notedeck_notebook/Cargo.toml b/crates/notedeck_notebook/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "notedeck_notebook"
+edition = "2024"
+version.workspace = true
+
+[dependencies]
+jsoncanvas = { git = "https://github.com/jb55/jsoncanvas", rev = "ae60f96e4d022cf037e086b793cacc3225bc14e5" }
+notedeck = { workspace = true }
+egui = { workspace = true }
diff --git a/crates/notedeck_notebook/demo.canvas b/crates/notedeck_notebook/demo.canvas
@@ -0,0 +1,232 @@
+{
+ "nodes": [
+ {
+ "id": "c450e9832ffe7a3d",
+ "type": "text",
+ "text": "```\nIncident Identifier: 3AAF0AF2-37A5-4104-957C-D0205F54B5BB\nDistributor ID: com.apple.TestFlight\nHardware Model: iPhone15,2\nProcess: DamusNotificationService [15759]\nPath: /private/var/containers/Bundle/Application/58C02D0F-9151-4F13-BD68-547D0D89A8E6/damus.app/PlugIns/DamusNotificationService.appex/DamusNotificationService\nIdentifier: com.jb55.damus2.DamusNotificationService\nVersion: 1.15 (1048)\nAppVariant: 1:iPhone15,2:18\nBeta: YES\nCode Type: ARM-64 (Native)\nRole: Unspecified\nParent Process: launchd [1]\nCoalition: com.jb55.damus2.DamusNotificationService [7022]\n\nDate/Time: 2025-07-14 12:26:33.8942 -1000\nLaunch Time: 2025-07-14 12:15:17.0329 -1000\nOS Version: iPhone OS 18.5 (22F76)\nRelease Type: User\nBaseband Version: 3.60.02\nReport Version: 104\n\nException Type: EXC_CRASH (SIGKILL)\nException Codes: 0x0000000000000000, 0x0000000000000000\nTermination Reason: RUNNINGBOARD 0xdead10cc \n\nTriggered by Thread: 0\n\n\nThread 0 name:\nThread 0 Crashed:\n0 libsystem_kernel.dylib \t0x00000001d9dc7ce4 mach_msg2_trap + 8\n1 libsystem_kernel.dylib \t0x00000001d9dcb39c mach_msg2_internal + 76 (mach_msg.c:201)\n2 libsystem_kernel.dylib \t0x00000001d9dcb2b8 mach_msg_overwrite + 428 (mach_msg.c:0)\n3 libsystem_kernel.dylib \t0x00000001d9dcb100 mach_msg + 24 (mach_msg.c:323)\n4 CoreFoundation \t0x0000000188bba900 __CFRunLoopServiceMachPort + 160 (CFRunLoop.c:2637)\n5 CoreFoundation \t0x0000000188bb91f0 __CFRunLoopRun + 1208 (CFRunLoop.c:3021)\n6 CoreFoundation \t0x0000000188bbac3c CFRunLoopRunSpecific + 572 (CFRunLoop.c:3434)\n7 Foundation \t0x000000018783279c -[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 212 (NSRunLoop.m:375)\n8 Foundation \t0x0000000187837108 -[NSRunLoop(NSRunLoop) run] + 64 (NSRunLoop.m:400)\n9 libxpc.dylib \t0x0000000213332d34 _xpc_objc_main + 336 (main.m:267)\n10 libxpc.dylib \t0x00000002133352a4 _xpc_main + 64 (init.c:1293)\n11 libxpc.dylib \t0x0000000213335484 xpc_main + 64 (init.c:1376)\n12 Foundation \t0x00000001879409dc -[NSXPCListener resume] + 308 (NSXPCListener.m:471)\n13 PlugInKit \t0x00000001b9e67f84 -[PKService runUsingServiceListener:] + 364 (PKService.m:219)\n14 PlugInKit \t0x00000001b9e67e10 -[PKService run] + 20 (PKService.m:185)\n15 PlugInKit \t0x00000001b9e67adc +[PKService main] + 520 (PKService.m:126)\n16 PlugInKit \t0x00000001b9e682d8 +[PKService _defaultRun:arguments:] + 16 (PKService.m:265)\n17 ExtensionFoundation \t0x000000019816fc20 EXExtensionMain + 288 (EXExtensionMain.m:0)\n18 Foundation \t0x00000001879ac274 NSExtensionMain + 200 (NSExtensionMain.m:21)\n19 dyld \t0x00000001afa8ff08 start + 6040 (dyldMain.cpp:1450)\n\nThread 1:\n0 libsystem_pthread.dylib \t0x00000002132ccaa4 start_wqthread + 0\n\nThread 2:\n0 libsystem_kernel.dylib \t0x00000001d9dc7c60 semaphore_wait_trap + 8\n1 libdispatch.dylib \t0x0000000190b368e0 _dispatch_sema4_wait + 28 (lock.c:139)\n2 libdispatch.dylib \t0x0000000190b36e90 _dispatch_semaphore_wait_slow + 132 (semaphore.c:132)\n3 CFNetwork \t0x000000018a27d12c CFURLConnectionSendSynchronousRequest + 356 (CFURLConnection_Synchronous.c:209)\n4 CFNetwork \t0x000000018a2982c0 +[NSURLConnection sendSynchronousRequest:returningResponse:error:] + 296 (NSURLConnection.mm:478)\n5 Foundation \t0x00000001878c9dec -[NSData(NSData) initWithContentsOfURL:options:maxLength:error:] + 240 (NSData.m:992)\n6 ImageIO \t0x000000018e7cabf8 IIOCreateDataWithContentsOfURL + 56 (IIOObjUtils.mm:50)\n7 ImageIO \t0x000000018e685cd4 IIOImageSource::IIOImageSource(__CFURL const*, IIODictionary*) + 920 (CGImageSource.cpp:561)\n8 ImageIO \t0x000000018e6856e8 CGImageSourceCreateWithURL + 260 (CGImageSource.cpp:4600)\n9 DamusNotificationService \t0x0000000102733770 closure #2 in NotificationService.didReceive(_:withContentHandler:) + 104 (NotificationService.swift:113)\n10 DamusNotificationService \t0x000000010273d03d <deduplicated_symbol> + 1\n11 DamusNotificationService \t0x0000000102778c55 specialized thunk for @escaping @isolated(any) @callee_guaranteed @async () -> (@out A) + 1 (/<compiler-generated>:0)\n12 DamusNotificationService \t0x000000010273e885 <deduplicated_symbol> + 1\n13 libswift_Concurrency.dylib \t0x000000019478d241 completeTaskWithClosure(swift::AsyncContext*, swift::SwiftError*) + 1 (Task.cpp:537)\n\nThread 3:\n0 libsystem_pthread.dylib \t0x00000002132ccaa4 start_wqthread + 0\n\nThread 4:\n0 libsystem_pthread.dylib \t0x00000002132ccaa4 start_wqthread + 0\n\nThread 5:\n0 libsystem_kernel.dylib \t0x00000001d9dcd438 __psynch_cvwait + 8\n1 libsystem_pthread.dylib \t0x00000002132cde50 _pthread_cond_wait + 984 (pthread_cond.c:862)\n2 DamusNotificationService \t0x0000000102710b90 prot_queue_pop_all + 52 (protected_queue.h:190)\n3 DamusNotificationService \t0x00000001027107ac ndb_writer_thread + 120 (nostrdb.c:2709)\n4 libsystem_pthread.dylib \t0x00000002132cf344 _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x00000002132ccab8 thread_start + 8\n\nThread 6:\n0 libsystem_kernel.dylib \t0x00000001d9dcd438 __psynch_cvwait + 8\n1 libsystem_pthread.dylib \t0x00000002132cde50 _pthread_cond_wait + 984 (pthread_cond.c:862)\n2 DamusNotificationService \t0x0000000102710b90 prot_queue_pop_all + 52 (protected_queue.h:190)\n3 DamusNotificationService \t0x00000001027115c0 ndb_ingester_thread + 156 (nostrdb.c:2809)\n4 libsystem_pthread.dylib \t0x00000002132cf344 _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x00000002132ccab8 thread_start + 8\n\nThread 7:\n0 libsystem_kernel.dylib \t0x00000001d9dcd438 __psynch_cvwait + 8\n1 libsystem_pthread.dylib \t0x00000002132cde50 _pthread_cond_wait + 984 (pthread_cond.c:862)\n2 DamusNotificationService \t0x0000000102710b90 prot_queue_pop_all + 52 (protected_queue.h:190)\n3 DamusNotificationService \t0x00000001027115c0 ndb_ingester_thread + 156 (nostrdb.c:2809)\n4 libsystem_pthread.dylib \t0x00000002132cf344 _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x00000002132ccab8 thread_start + 8\n\nThread 8:\n0 libsystem_kernel.dylib \t0x00000001d9dcd438 __psynch_cvwait + 8\n1 libsystem_pthread.dylib \t0x00000002132cde50 _pthread_cond_wait + 984 (pthread_cond.c:862)\n2 DamusNotificationService \t0x0000000102710b90 prot_queue_pop_all + 52 (protected_queue.h:190)\n3 DamusNotificationService \t0x00000001027115c0 ndb_ingester_thread + 156 (nostrdb.c:2809)\n4 libsystem_pthread.dylib \t0x00000002132cf344 _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x00000002132ccab8 thread_start + 8\n\nThread 9:\n0 libsystem_kernel.dylib \t0x00000001d9dcd438 __psynch_cvwait + 8\n1 libsystem_pthread.dylib \t0x00000002132cde50 _pthread_cond_wait + 984 (pthread_cond.c:862)\n2 DamusNotificationService \t0x0000000102710b90 prot_queue_pop_all + 52 (protected_queue.h:190)\n3 DamusNotificationService \t0x00000001027115c0 ndb_ingester_thread + 156 (nostrdb.c:2809)\n4 libsystem_pthread.dylib \t0x00000002132cf344 _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x00000002132ccab8 thread_start + 8\n\nThread 10:\n0 libsystem_pthread.dylib \t0x00000002132ccaa4 start_wqthread + 0\n\n\nThread 0 crashed with ARM Thread State (64-bit):\n x0: 0x0000000010004005 x1: 0x0000000507000806 x2: 0x0000000200000000 x3: 0x0000240300000000\n x4: 0x0000000000000000 x5: 0x0000240300000000 x6: 0x0000000000000002 x7: 0x00000000ffffffff\n x8: 0x0000000000000000 x9: 0x0000000000000000 x10: 0x0000000000000002 x11: 0x0000000000000000\n x12: 0x0000000000000000 x13: 0x0000000000002403 x14: 0x0000000000000000 x15: 0x0000000000000000\n x16: 0xffffffffffffffd1 x17: 0x0000000000000002 x18: 0x0000000000000000 x19: 0x00000000ffffffff\n x20: 0x0000000000000002 x21: 0x0000240300000000 x22: 0x0000000000000000 x23: 0x0000240300000000\n x24: 0x000000016d702058 x25: 0x0000000200000000 x26: 0x0000000507000806 x27: 0xfffffffffffffbbf\n x28: 0x00000001f38ed000 fp: 0x000000016d701fc0 lr: 0x00000001d9dcb39c\n sp: 0x000000016d701f70 pc: 0x00000001d9dc7ce4 cpsr: 0x1000\n esr: 0x56000080 Address size fault\n\n\nBinary Images:\n 0x1026fc000 - 0x102aaffff DamusNotificationService arm64 <a3f29d049d8d30afa8e2a546b137c2a3> /private/var/containers/Bundle/Application/58C02D0F-9151-4F13-BD68-547D0D89A8E6/damus.app/PlugIns/DamusNotificationService.appex/DamusNotificationService\n 0x187823000 - 0x188496ddf Foundation arm64e <34de055d8683380a9198c3347211d13d> /System/Library/Frameworks/Foundation.framework/Foundation\n 0x188ba9000 - 0x189125fff CoreFoundation arm64e <7821f73c378b3a10be90ef526b7dba93> /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation\n 0x18a13a000 - 0x18a4ffb9f CFNetwork arm64e <a35a109c49d23986965d4ed7e0b6681e> /System/Library/Frameworks/CFNetwork.framework/CFNetwork\n 0x18e65e000 - 0x18ec3259f ImageIO arm64e <10cc4cb3264c3269b6a2dc7a2d7b2179> /System/Library/Frameworks/ImageIO.framework/ImageIO\n 0x190b33000 - 0x190b78b1f libdispatch.dylib arm64e <395da84f715d334e8d41a16cd93fc83c> /usr/lib/system/libdispatch.dylib\n 0x194728000 - 0x1947a7a3f libswift_Concurrency.dylib arm64e <dcb9e73a92ba3782bc6d3e1906622689> /usr/lib/swift/libswift_Concurrency.dylib\n 0x198147000 - 0x19820363f ExtensionFoundation arm64e <c7396624315c328aa5c247cfe6e3f88e> /System/Library/Frameworks/ExtensionFoundation.framework/ExtensionFoundation\n 0x1afa51000 - 0x1afaeb857 dyld arm64e <86d5253d4fd136f3b4ab25982c90cbf4> /usr/lib/dyld\n 0x1b9e4f000 - 0x1b9e89a7f PlugInKit arm64e <931fdec36ed5300796a4eea5aadd47bb> /System/Library/PrivateFrameworks/PlugInKit.framework/PlugInKit\n 0x1d9dc7000 - 0x1d9e00ebf libsystem_kernel.dylib arm64e <9e195be11733345ea9bf50d0d7059647> /usr/lib/system/libsystem_kernel.dylib\n 0x2132cc000 - 0x2132d83f3 libsystem_pthread.dylib arm64e <b37430d8e3af33e481e1faed9ee26e8a> /usr/lib/system/libsystem_pthread.dylib\n 0x213317000 - 0x21335edbf libxpc.dylib arm64e <a46c2755958633b89ea9377f71175516> /usr/lib/system/libxpc.dylib\n\nEOF\n```",
+ "x": -69,
+ "y": -82,
+ "width": 889,
+ "height": 1062
+ },
+ {
+ "id": "8ba807ac67740bc0",
+ "type": "text",
+ "text": "# 1st most common crash",
+ "x": 167,
+ "y": -320,
+ "width": 373,
+ "height": 80
+ },
+ {
+ "id": "906aae4882968fc5",
+ "type": "text",
+ "text": "`0xdead10cc` (`3735883980`) — pronounced “dead lock”\n\nThe operating system terminated the app because it held on to a file lock or SQLite database lock during suspension. Request additional background execution time on the main thread with [`beginBackgroundTask(withName:expirationHandler:)`](https://developer.apple.com/documentation/UIKit/UIApplication/beginBackgroundTask\\(withName:expirationHandler:\\)). Make this request well before starting to write to the file in order to complete those operations and relinquish the lock before the app suspends. In an app extension, use [`beginActivity(options:reason:)`](https://developer.apple.com/documentation/Foundation/ProcessInfo/beginActivity\\(options:reason:\\)) to manage this work.",
+ "x": -480,
+ "y": 1260,
+ "width": 659,
+ "height": 273
+ },
+ {
+ "id": "e619c24d803fc838",
+ "type": "text",
+ "text": "Holding ndb db file in notification target",
+ "x": 279,
+ "y": 1192,
+ "width": 281,
+ "height": 108,
+ "color": "2"
+ },
+ {
+ "id": "4f83ab4aa30b00f1",
+ "type": "text",
+ "text": "```\nIncident Identifier: F46AD3DE-40FA-483E-9E70-D48DAB7643E6\nDistributor ID: com.apple.TestFlight\nHardware Model: iPhone16,1\nProcess: damus [1140]\nPath: /private/var/containers/Bundle/Application/3D9243CC-8814-4347-80D5-C0BACEF9D2CD/damus.app/damus\nIdentifier: com.jb55.damus2\nVersion: 1.12 (682)\nAppStoreTools: 16C5031b\nAppVariant: 1:iPhone16,1:18\nBeta: YES\nCode Type: ARM-64 (Native)\nRole: unknown\nParent Process: launchd [1]\nCoalition: com.jb55.damus2 [1234]\n\nDate/Time: 2024-12-21 13:26:52.2609 +0800\nLaunch Time: 2024-12-21 13:26:50.6386 +0800\nOS Version: iPhone OS 18.1.1 (22B91)\nRelease Type: User\nBaseband Version: 2.20.03\nReport Version: 104\n\nException Type: EXC_CRASH (SIGKILL)\nException Codes: 0x0000000000000000, 0x0000000000000000\nTermination Reason: RUNNINGBOARD 0xdead10cc \n\nTriggered by Thread: 0\n\n\nThread 0 name:\nThread 0 Crashed:\n0 libsystem_kernel.dylib \t0x00000001e4352688 mach_msg2_trap + 8 (:-1)\n1 libsystem_kernel.dylib \t0x00000001e4355d98 mach_msg2_internal + 80 (mach_msg.c:201)\n2 libsystem_kernel.dylib \t0x00000001e4355cb0 mach_msg_overwrite + 424 (mach_msg.c:0)\n3 libsystem_kernel.dylib \t0x00000001e4355afc mach_msg + 24 (mach_msg.c:323)\n4 CoreFoundation \t0x0000000193f91a84 __CFRunLoopServiceMachPort + 160 (CFRunLoop.c:2637)\n5 CoreFoundation \t0x0000000193f91130 __CFRunLoopRun + 1212 (CFRunLoop.c:3021)\n6 CoreFoundation \t0x0000000193f90830 CFRunLoopRunSpecific + 588 (CFRunLoop.c:3434)\n7 GraphicsServices \t0x00000001dff701c4 GSEventRunModal + 164 (GSEvent.c:2196)\n8 UIKitCore \t0x0000000196af6eb0 -[UIApplication _run] + 816 (UIApplication.m:3844)\n9 UIKitCore \t0x0000000196ba55b4 UIApplicationMain + 340 (UIApplication.m:5496)\n10 SwiftUI \t0x00000001986f9f98 closure #1 in KitRendererCommon(_:) + 168 (UIKitApp.swift:68)\n11 SwiftUI \t0x00000001986da664 runApp<A>(_:) + 100 (UIKitApp.swift:16)\n12 SwiftUI \t0x00000001986dd490 static App.main() + 180 (App.swift:121)\n13 damus \t0x000000010516f630 static damusApp.$main() + 56 (damusApp.swift:0)\n14 damus \t0x000000010516f630 main + 68\n15 dyld \t0x00000001b997eec8 start + 2724 (dyldMain.cpp:1334)\n\nThread 1:\n0 libsystem_pthread.dylib \t0x000000021c4c4480 start_wqthread + 0 (:-1)\n\nThread 2:\n0 libsystem_pthread.dylib \t0x000000021c4c4480 start_wqthread + 0 (:-1)\n\nThread 3:\n0 libsystem_pthread.dylib \t0x000000021c4c4480 start_wqthread + 0 (:-1)\n\nThread 4:\n0 libsystem_pthread.dylib \t0x000000021c4c4480 start_wqthread + 0 (:-1)\n\nThread 5:\n0 libsystem_pthread.dylib \t0x000000021c4c4480 start_wqthread + 0 (:-1)\n\nThread 6:\n0 libsystem_pthread.dylib \t0x000000021c4c4480 start_wqthread + 0 (:-1)\n\nThread 7:\n0 libsystem_pthread.dylib \t0x000000021c4c4480 start_wqthread + 0 (:-1)\n\nThread 8:\n0 libsystem_pthread.dylib \t0x000000021c4c4480 start_wqthread + 0 (:-1)\n\nThread 9 name:\nThread 9:\n0 libsystem_kernel.dylib \t0x00000001e4352688 mach_msg2_trap + 8 (:-1)\n1 libsystem_kernel.dylib \t0x00000001e4355d98 mach_msg2_internal + 80 (mach_msg.c:201)\n2 libsystem_kernel.dylib \t0x00000001e4355cb0 mach_msg_overwrite + 424 (mach_msg.c:0)\n3 libsystem_kernel.dylib \t0x00000001e4355afc mach_msg + 24 (mach_msg.c:323)\n4 CoreFoundation \t0x0000000193f91a84 __CFRunLoopServiceMachPort + 160 (CFRunLoop.c:2637)\n5 CoreFoundation \t0x0000000193f91130 __CFRunLoopRun + 1212 (CFRunLoop.c:3021)\n6 CoreFoundation \t0x0000000193f90830 CFRunLoopRunSpecific + 588 (CFRunLoop.c:3434)\n7 Foundation \t0x0000000192c38500 -[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 212 (NSRunLoop.m:373)\n8 Foundation \t0x0000000192c38350 -[NSRunLoop(NSRunLoop) runUntilDate:] + 64 (NSRunLoop.m:420)\n9 UIKitCore \t0x0000000196b0a358 -[UIEventFetcher threadMain] + 420 (UIEventFetcher.m:1241)\n10 Foundation \t0x0000000192c496c8 __NSThread__start__ + 724 (NSThread.m:991)\n11 libsystem_pthread.dylib \t0x000000021c4c937c _pthread_start + 136 (pthread.c:931)\n12 libsystem_pthread.dylib \t0x000000021c4c4494 thread_start + 8 (:-1)\n\nThread 10:\n0 libsystem_kernel.dylib \t0x00000001e4357f90 __psynch_cvwait + 8 (:-1)\n1 libsystem_pthread.dylib \t0x000000021c4c6a50 _pthread_cond_wait + 1204 (pthread_cond.c:862)\n2 damus \t0x0000000104e6703c prot_queue_pop_all + 60 (protected_queue.h:190)\n3 damus \t0x0000000104e66c5c ndb_writer_thread + 140 (nostrdb.c:2709)\n4 libsystem_pthread.dylib \t0x000000021c4c937c _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x000000021c4c4494 thread_start + 8 (:-1)\n\nThread 11:\n0 libsystem_kernel.dylib \t0x00000001e4357f90 __psynch_cvwait + 8 (:-1)\n1 libsystem_pthread.dylib \t0x000000021c4c6a50 _pthread_cond_wait + 1204 (pthread_cond.c:862)\n2 damus \t0x0000000104e6703c prot_queue_pop_all + 60 (protected_queue.h:190)\n3 damus \t0x0000000104e67a7c ndb_ingester_thread + 168 (nostrdb.c:2809)\n4 libsystem_pthread.dylib \t0x000000021c4c937c _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x000000021c4c4494 thread_start + 8 (:-1)\n\nThread 12:\n0 libsystem_kernel.dylib \t0x00000001e4357f90 __psynch_cvwait + 8 (:-1)\n1 libsystem_pthread.dylib \t0x000000021c4c6a50 _pthread_cond_wait + 1204 (pthread_cond.c:862)\n2 damus \t0x0000000104e6703c prot_queue_pop_all + 60 (protected_queue.h:190)\n3 damus \t0x0000000104e67a7c ndb_ingester_thread + 168 (nostrdb.c:2809)\n4 libsystem_pthread.dylib \t0x000000021c4c937c _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x000000021c4c4494 thread_start + 8 (:-1)\n\nThread 13:\n0 libsystem_kernel.dylib \t0x00000001e4357f90 __psynch_cvwait + 8 (:-1)\n1 libsystem_pthread.dylib \t0x000000021c4c6a50 _pthread_cond_wait + 1204 (pthread_cond.c:862)\n2 damus \t0x0000000104e6703c prot_queue_pop_all + 60 (protected_queue.h:190)\n3 damus \t0x0000000104e67a7c ndb_ingester_thread + 168 (nostrdb.c:2809)\n4 libsystem_pthread.dylib \t0x000000021c4c937c _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x000000021c4c4494 thread_start + 8 (:-1)\n\nThread 14:\n0 libsystem_kernel.dylib \t0x00000001e4357f90 __psynch_cvwait + 8 (:-1)\n1 libsystem_pthread.dylib \t0x000000021c4c6a50 _pthread_cond_wait + 1204 (pthread_cond.c:862)\n2 damus \t0x0000000104e6703c prot_queue_pop_all + 60 (protected_queue.h:190)\n3 damus \t0x0000000104e67a7c ndb_ingester_thread + 168 (nostrdb.c:2809)\n4 libsystem_pthread.dylib \t0x000000021c4c937c _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x000000021c4c4494 thread_start + 8 (:-1)\n\nThread 15:\n0 libsystem_pthread.dylib \t0x000000021c4c4480 start_wqthread + 0 (:-1)\n\nThread 16 name:\nThread 16:\n0 libsystem_kernel.dylib \t0x00000001e435261c semaphore_timedwait_trap + 8 (:-1)\n1 libdispatch.dylib \t0x000000019bc966e8 _dispatch_sema4_timedwait + 64 (lock.c:154)\n2 libdispatch.dylib \t0x000000019bc96ce8 _dispatch_semaphore_wait_slow + 76 (semaphore.c:116)\n3 libdispatch.dylib \t0x000000019bca7b60 _dispatch_worker_thread + 324 (queue.c:7509)\n4 libsystem_pthread.dylib \t0x000000021c4c937c _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x000000021c4c4494 thread_start + 8 (:-1)\n\n\nThread 0 crashed with ARM Thread State (64-bit):\n x0: 0x0000000010004005 x1: 0x0000000507000806 x2: 0x0000000200000000 x3: 0x00002e0300000000\n x4: 0x0000000000000000 x5: 0x00002e0300000000 x6: 0x0000000000000002 x7: 0x00000000ffffffff\n x8: 0xfffffffffffffbbf x9: 0x0000000000000002 x10: 0x0000000000000000 x11: 0x0000000000000000\n x12: 0x0000000000000000 x13: 0x0000000000002e03 x14: 0x0000000000000000 x15: 0x0000000000000000\n x16: 0xffffffffffffffd1 x17: 0x000000019bcde358 x18: 0x0000000000000000 x19: 0x00000000ffffffff\n x20: 0x0000000000000002 x21: 0x00002e0300000000 x22: 0x0000000000000000 x23: 0x00002e0300000000\n x24: 0x000000016afae6d8 x25: 0x0000000200000000 x26: 0x0000000507000806 x27: 0x0000000507000806\n x28: 0x0000000107000806 fp: 0x000000016afae640 lr: 0x00000001e4355d98\n sp: 0x000000016afae5f0 pc: 0x00000001e4352688 cpsr: 0x1000\n esr: 0x56000080 Address size fault\n\n\nBinary Images:\n 0x104e50000 - 0x105653fff damus arm64 <43d5ad08c0d13100afedd27aeb88d9ff> /private/var/containers/Bundle/Application/3D9243CC-8814-4347-80D5-C0BACEF9D2CD/damus.app/damus\n 0x1062c8000 - 0x1062d3fff libobjc-trampolines.dylib arm64e <35a44678195b39c2bdd7072893564b45> /private/preboot/Cryptexes/OS/usr/lib/libobjc-trampolines.dylib\n 0x192b81000 - 0x19388efff Foundation arm64e <6d0212cc3b9e32c9be2072989ce3acb8> /System/Library/Frameworks/Foundation.framework/Foundation\n 0x193f3e000 - 0x194480fff CoreFoundation arm64e <1532d3d89b3b3f2fb35f55a20ddf411b> /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation\n 0x196724000 - 0x1985f7fff UIKitCore arm64e <575e5140fa6a37c2b00ba4eacedfda53> /System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore\n 0x1985f8000 - 0x1998ecfff SwiftUI arm64e <9f67c19cfcde3e979fc23bba36998297> /System/Library/Frameworks/SwiftUI.framework/SwiftUI\n 0x19bc92000 - 0x19bcd7fff libdispatch.dylib arm64e <7de7ec03cfb7349d9b9e8782b38f231d> /usr/lib/system/libdispatch.dylib\n 0x19bcd8000 - 0x19bd57ff3 libsystem_c.dylib arm64e <0150f750db0a3f54b23ad21c55af8824> /usr/lib/system/libsystem_c.dylib\n 0x1a5e11000 - 0x1a6060fff MediaExperience arm64e <e2f69071040233a7ba6dc247b73ec092> /System/Library/PrivateFrameworks/MediaExperience.framework/MediaExperience\n 0x1b994b000 - 0x1b99ce99f dyld arm64e <3060d36a16ce3c3a92583881459f5714> /usr/lib/dyld\n 0x1dff6f000 - 0x1dff77fff GraphicsServices arm64e <8425ea11000e3e5e8abcbddf3ff3fa32> /System/Library/PrivateFrameworks/GraphicsServices.framework/GraphicsServices\n 0x1e4351000 - 0x1e438aff3 libsystem_kernel.dylib arm64e <b9618c71c0cb31b6825f92a4737c890e> /usr/lib/system/libsystem_kernel.dylib\n 0x21c4c3000 - 0x21c4cfff3 libsystem_pthread.dylib arm64e <3ca98e388eee3c269862c5f66aad93c0> /usr/lib/system/libsystem_pthread.dylib\n\nEOF\n```",
+ "x": 2320,
+ "y": -82,
+ "width": 1060,
+ "height": 1120
+ },
+ {
+ "id": "b27baddab9517a38",
+ "type": "text",
+ "text": "# 2nd most common crash",
+ "x": 1415,
+ "y": -300,
+ "width": 385,
+ "height": 77
+ },
+ {
+ "id": "0df74aca0192124e",
+ "type": "text",
+ "text": "Holding ndb db file in main target",
+ "x": 2710,
+ "y": 1160,
+ "width": 281,
+ "height": 108,
+ "color": "2"
+ },
+ {
+ "id": "e48e5b32c0265d73",
+ "type": "text",
+ "text": "# 3rd most common crash",
+ "x": 2635,
+ "y": -280,
+ "width": 430,
+ "height": 102
+ },
+ {
+ "id": "c04db70a0e694555",
+ "type": "text",
+ "text": "```swift\nlet sender_profile = {\n let txn = state.ndb.lookup_profile(nostr_event.pubkey)\n let profile = txn?.unsafeUnownedValue?.profile\n let picture = ((profile?.picture.map { URL(string: $0) }) ?? URL(string: robohash(nostr_event.pubkey)))! // <-- Force unwrap error\n return ProfileBuf(picture: picture,\n name: profile?.name,\n display_name: profile?.display_name,\n nip05: profile?.nip05)\n }()\n```",
+ "x": 3596,
+ "y": 45,
+ "width": 1384,
+ "height": 375
+ },
+ {
+ "id": "d275d9d3cca27202",
+ "type": "text",
+ "text": "# 4th most common crash",
+ "x": 4073,
+ "y": -240,
+ "width": 430,
+ "height": 102
+ },
+ {
+ "id": "39b704e37669fc83",
+ "type": "text",
+ "text": "```\nIncident Identifier: 0B12A949-43C3-4B05-820B-643210EAA9FB\nDistributor ID: com.apple.TestFlight\nHardware Model: iPhone14,4\nProcess: damus [2492]\nPath: /private/var/containers/Bundle/Application/7FFD6048-6DD6-4092-A83F-B80314A637A5/damus.app/damus\nIdentifier: com.jb55.damus2\nVersion: 1.14 (914)\nAppStoreTools: 16E137\nAppVariant: 1:iPhone14,4:18\nBeta: YES\nCode Type: ARM-64 (Native)\nRole: Background\nParent Process: launchd [1]\nCoalition: com.jb55.damus2 [1048]\n\nDate/Time: 2025-04-16 14:36:08.5474 -0700\nLaunch Time: 2025-04-16 14:34:59.0919 -0700\nOS Version: iPhone OS 18.4 (22E240)\nRelease Type: User\nBaseband Version: 4.51.04\nReport Version: 104\n\nException Type: EXC_BAD_ACCESS (SIGSEGV)\nException Subtype: KERN_INVALID_ADDRESS at 0x00000000000000d0\nException Codes: 0x0000000000000001, 0x00000000000000d0\nVM Region Info: 0xd0 is not in any region. Bytes before following region: 4331470640\n REGION TYPE START - END [ VSIZE] PRT/MAX SHRMOD REGION DETAIL\n UNUSED SPACE AT START\n---> \n __TEXT 1022d0000-102618000 [ 3360K] r-x/r-x SM=COW /var/containers/Bundle/Application/7FFD6048-6DD6-4092-A83F-B80314A637A5/damus.app/damus\nTermination Reason: SIGNAL 11 Segmentation fault: 11\nTerminating Process: exc handler [2492]\n\nTriggered by Thread: 38\n```\n\n```\nThread 38 name:\nThread 38 Crashed:\n0 libsystem_pthread.dylib \t0x00000002234344c0 pthread_mutex_lock + 12 (pthread_mutex.c:1709)\n1 damus \t0x00000001022e5a28 prot_queue_push + 32 (protected_queue.h:91)\n2 damus \t0x00000001022e4570 threadpool_dispatch + 48 (threadpool.h:76)\n3 damus \t0x00000001022e4570 ndb_ingester_queue_event + 64 (nostrdb.c:2950)\n4 damus \t0x00000001022e4570 ndb_process_event + 136 (nostrdb.c:3211)\n5 damus \t0x0000000102305394 closure #1 in Ndb.process_client_event(_:) + 76\n6 damus \t0x0000000102305340 closure #1 in Ndb.process_event(_:) + 20\n7 damus \t0x0000000102353f08 partial apply for closure #1 in Ndb.process_event(_:) + 20 (/<compiler-generated>:0)\n8 libswiftCore.dylib \t0x000000019776c384 _StringGuts._slowWithCString<A>(_:) + 76 (StringGuts.swift:239)\n9 damus \t0x0000000102702420 specialized String.withCString<A>(_:) + 176 (/<compiler-generated>:0)\n10 damus \t0x000000010234e70c Ndb.process_event(_:) + 28 (Ndb.swift:530)\n11 damus \t0x000000010234e70c $s5damus9RelayPoolC9add_relayyyAC0B10DescriptorVAC0B5ErrorOYKFyAA14WebSocketEventOcfU0_ + 436 (RelayPool.swift:139)\n12 damus \t0x00000001025e8bb4 RelayConnection.receive(event:) + 416 (RelayConnection.swift:118)\n13 damus \t0x00000001025e9670 closure #2 in RelayConnection.connect(force:) + 68 (RelayConnection.swift:79)\n14 Combine \t0x00000001a2605b18 Subscribers.Sink.receive(_:) + 92 (Sink.swift:128)\n15 Combine \t0x00000001a2605aac protocol witness for Subscriber.receive(_:) in conformance Subscribers.Sink<A, B> + 24 (<compiler-generated>:0)\n16 Combine \t0x00000001a260494c closure #1 in Publishers.ReceiveOn.Inner.receive(_:) + 284 (ReceiveOn.swift:169)\n17 libswiftDispatch.dylib \t0x00000001a2525a48 thunk for @escaping @callee_guaranteed () -> () + 36 (:-1)\n18 libdispatch.dylib \t0x00000001a0e49aac _dispatch_call_block_and_release + 32 (init.c:1575)\n19 libdispatch.dylib \t0x00000001a0e63584 _dispatch_client_callout + 16 (client_callout.mm:85)\n20 libdispatch.dylib \t0x00000001a0e7fe84 <deduplicated_symbol> + 32 (:-1)\n21 libdispatch.dylib \t0x00000001a0e5bf24 _dispatch_root_queue_drain + 736 (queue.c:7342)\n22 libdispatch.dylib \t0x00000001a0e5c54c _dispatch_worker_thread2 + 156 (queue.c:7410)\n23 libsystem_pthread.dylib \t0x0000000223435624 _pthread_wqthread + 232 (pthread.c:2709)\n24 libsystem_pthread.dylib \t0x00000002234329f8 start_wqthread + 8 (:-1)\n```",
+ "x": 5120,
+ "y": -2,
+ "width": 1240,
+ "height": 1722
+ },
+ {
+ "id": "e182bb08633f0c8e",
+ "type": "text",
+ "text": "# 5th most common crash",
+ "x": 5525,
+ "y": -240,
+ "width": 430,
+ "height": 102
+ },
+ {
+ "id": "a7b2dd98e8fb8b98",
+ "type": "text",
+ "text": "Unsafe \"force unwrap\"",
+ "x": 4157,
+ "y": 489,
+ "width": 250,
+ "height": 60,
+ "color": "2"
+ },
+ {
+ "id": "a3718eb8e2f9f28d",
+ "type": "text",
+ "text": "Probably memory or concurrency issue related to Ndb",
+ "x": 5619,
+ "y": 1899,
+ "width": 341,
+ "height": 101,
+ "color": "2"
+ },
+ {
+ "id": "915e462d7852f6bb",
+ "type": "text",
+ "text": "Memory error related to ndb",
+ "x": 1483,
+ "y": 2520,
+ "width": 250,
+ "height": 60,
+ "color": "2"
+ },
+ {
+ "id": "3d398a7223484f22",
+ "type": "text",
+ "text": "```\nIncident Identifier: CF913FAA-5091-4EF7-8D0B-E29C25BFC366\nDistributor ID: com.apple.TestFlight\nHardware Model: iPhone17,2\nProcess: DamusNotificationService [35444]\nPath: /private/var/containers/Bundle/Application/C48A3744-BAD0-41F2-812F-AA9F65FE651E/damus.app/PlugIns/DamusNotificationService.appex/DamusNotificationService\nIdentifier: com.jb55.damus2.DamusNotificationService\nVersion: 1.14 (914)\nAppVariant: 1:iPhone17,2:18\nBeta: YES\nCode Type: ARM-64 (Native)\nRole: Unspecified\nParent Process: launchd [1]\nCoalition: com.jb55.damus2.DamusNotificationService [1098]\n\nDate/Time: 2025-04-22 11:08:47.5725 -0700\nLaunch Time: 2025-04-22 10:20:47.0779 -0700\nOS Version: iPhone OS 18.3.2 (22D82)\nRelease Type: User\nBaseband Version: 1.40.03\nReport Version: 104\n\nException Type: EXC_BAD_ACCESS (SIGSEGV)\nException Subtype: KERN_INVALID_ADDRESS at 0x0000000000000009\nException Codes: 0x0000000000000001, 0x0000000000000009\nVM Region Info: 0x9 is not in any region. Bytes before following region: 4332535799\n REGION TYPE START - END [ VSIZE] PRT/MAX SHRMOD REGION DETAIL\n UNUSED SPACE AT START\n---> \n __TEXT 1023d4000-102608000 [ 2256K] r-x/r-x SM=COW /var/containers/Bundle/Application/C48A3744-BAD0-41F2-812F-AA9F65FE651E/damus.app/PlugIns/DamusNotificationService.appex/DamusNotificationService\nTermination Reason: SIGNAL 11 Segmentation fault: 11\nTerminating Process: exc handler [35444]\n\nTriggered by Thread: 1\n\n\nThread 0 name:\nThread 0:\n0 libsystem_kernel.dylib \t0x00000001e63b0788 mach_msg2_trap + 8 (:-1)\n1 libsystem_kernel.dylib \t0x00000001e63b3e98 mach_msg2_internal + 80 (mach_msg.c:201)\n2 libsystem_kernel.dylib \t0x00000001e63b3db0 mach_msg_overwrite + 424 (mach_msg.c:0)\n3 libsystem_kernel.dylib \t0x00000001e63b3bfc mach_msg + 24 (mach_msg.c:323)\n4 CoreFoundation \t0x0000000194ccb804 __CFRunLoopServiceMachPort + 160 (CFRunLoop.c:2637)\n5 CoreFoundation \t0x0000000194ccaeb0 __CFRunLoopRun + 1212 (CFRunLoop.c:3021)\n6 CoreFoundation \t0x0000000194d1d284 CFRunLoopRunSpecific + 588 (CFRunLoop.c:3434)\n7 Foundation \t0x00000001938830e8 -[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 212 (NSRunLoop.m:373)\n8 Foundation \t0x000000019387f2b0 -[NSRunLoop(NSRunLoop) run] + 64 (NSRunLoop.m:398)\n9 libxpc.dylib \t0x000000021fa7fdb4 _xpc_objc_main + 336 (main.m:267)\n10 libxpc.dylib \t0x000000021fa82320 _xpc_main + 64 (init.c:1291)\n11 libxpc.dylib \t0x000000021fa82500 xpc_main + 64 (init.c:1374)\n12 Foundation \t0x0000000193a509a8 -[NSXPCListener resume] + 308 (NSXPCListener.m:471)\n13 PlugInKit \t0x00000001c5a3944c -[PKService runUsingServiceListener:] + 364 (PKService.m:219)\n14 PlugInKit \t0x00000001c5a392d8 -[PKService run] + 20 (PKService.m:185)\n15 PlugInKit \t0x00000001c5a38f94 +[PKService main] + 524 (PKService.m:126)\n16 PlugInKit \t0x00000001c5a397a8 +[PKService _defaultRun:arguments:] + 16 (PKService.m:265)\n17 ExtensionFoundation \t0x00000001a401e724 EXExtensionMain + 288 (EXExtensionMain.m:0)\n18 Foundation \t0x0000000193b17350 NSExtensionMain + 204 (NSExtensionMain.m:21)\n19 dyld \t0x00000001baf79de8 start + 2724 (dyldMain.cpp:1338)\n\nThread 1 name:\nThread 1 Crashed:\n0 DamusNotificationService \t0x00000001023e0fe8 mdb_cursor_open + 60 (mdb.c:7687)\n1 DamusNotificationService \t0x00000001023e4270 ndb_get_tsid + 92 (nostrdb.c:1335)\n2 DamusNotificationService \t0x00000001023e4384 ndb_lookup_tsid + 64 (nostrdb.c:1390)\n3 DamusNotificationService \t0x000000010241e13c specialized closure #1 in Ndb.lookup_profile_with_txn_inner<A>(pubkey:txn:) + 100 (Ndb.swift:416)\n4 DamusNotificationService \t0x000000010241e3e8 specialized closure #1 in Ndb.lookup_profile_with_txn_inner<A>(pubkey:txn:) + 16 (/<compiler-generated>:0)\n5 DamusNotificationService \t0x000000010241e3e8 specialized closure #1 in Ndb.lookup_profile_with_txn_inner<A>(pubkey:txn:) + 16\n6 DamusNotificationService \t0x000000010241e3e8 specialized __DataStorage.withUnsafeBytes<A>(in:apply:) + 20\n7 DamusNotificationService \t0x000000010241e3e8 specialized __DataStorage.withUnsafeBytes<A>(in:apply:) + 20\n8 DamusNotificationService \t0x000000010241e3e8 specialized Data._Representation.withUnsafeBytes<A>(_:) + 508\n9 DamusNotificationService \t0x000000010241e3e8 0x1023d4000 + 304104 (/<compiler-generated>:0)\n10 DamusNotificationService \t0x000000010241e3e8 0x1023d4000 + 304104 (/<compiler-generated>:0)\n11 DamusNotificationService \t0x000000010241e3e8 0x1023d4000 + 304104 (/<compiler-generated>:0)\n12 DamusNotificationService \t0x000000010241e3e8 0x1023d4000 + 304104 (/<compiler-generated>:0)\n13 DamusNotificationService \t0x000000010241e9d8 specialized Data._Representation.withUnsafeBytes<A>(_:) + 12 (Ndb.swift:0)\n14 DamusNotificationService \t0x000000010241e9d8 specialized Ndb.lookup_profile_with_txn_inner<A>(pubkey:txn:) + 12 (Ndb.swift:492)\n15 DamusNotificationService \t0x000000010241e9d8 closure #1 in Ndb.lookup_profile(_:txn_name:) + 12 (/<compiler-generated>:0)\n16 DamusNotificationService \t0x000000010241e9d8 specialized NdbTxn.init(ndb:with:name:) + 1220\n17 DamusNotificationService \t0x000000010240b094 specialized NdbTxn.init(ndb:with:name:) + 28 (/<compiler-generated>:0)\n18 DamusNotificationService \t0x000000010240b094 Ndb.lookup_profile(_:txn_name:) + 28 (Ndb.swift:56)\n19 DamusNotificationService \t0x000000010240b094 closure #1 in NotificationService.didReceive(_:withContentHandler:) + 348 (NotificationService.swift:61)\n20 DamusNotificationService \t0x0000000102414170 specialized NotificationService.didReceive(_:withContentHandler:) + 1144 (NotificationService.swift:60)\n21 DamusNotificationService \t0x000000010240e0fc specialized NotificationService.didReceive(_:withContentHandler:) + 12 (/<compiler-generated>:0)\n22 DamusNotificationService \t0x000000010240e0fc @objc NotificationService.didReceive(_:withContentHandler:) + 76\n23 UserNotifications \t0x00000001c089a73c -[_UNNotificationServiceExtensionRemoteContext didReceiveNotificationRequest:withCompletionHandler:] + 356 (_UNNotificationServiceExtensionRemoteContext.m:55)\n24 Foundation \t0x0000000193910d8c __NSXPCCONNECTION_IS_CALLING_OUT_TO_EXPORTED_OBJECT_S1__ + 16 (NSXPCConnection.m:182)\n25 Foundation \t0x000000019390ffc8 -[NSXPCConnection _decodeAndInvokeMessageWithEvent:reply:flags:] + 1632 (NSXPCConnection.m:706)\n26 Foundation \t0x000000019390f220 message_handler_message + 88 (NSXPCConnection.m:824)\n27 Foundation \t0x000000019390f0d8 message_handler + 152 (NSXPCConnection.m:795)\n28 libxpc.dylib \t0x000000021fa75a50 _xpc_connection_call_event_handler + 144 (connection.c:837)\n29 libxpc.dylib \t0x000000021fa775cc _xpc_connection_mach_event + 1140 (connection.c:2457)\n30 libdispatch.dylib \t0x000000019ca17068 _dispatch_client_callout4 + 20 (object.m:616)\n31 libdispatch.dylib \t0x000000019ca33424 _dispatch_mach_msg_invoke + 464 (mach.c:2511)\n32 libdispatch.dylib \t0x000000019ca1e42c _dispatch_lane_serial_drain + 352 (queue.c:3934)\n33 libdispatch.dylib \t0x000000019ca34178 _dispatch_mach_invoke + 456 (mach.c:2861)\n34 libdispatch.dylib \t0x000000019ca1e42c _dispatch_lane_serial_drain + 352 (queue.c:3934)\n35 libdispatch.dylib \t0x000000019ca1f158 _dispatch_lane_invoke + 432 (queue.c:4025)\n36 libdispatch.dylib \t0x000000019ca2a38c _dispatch_root_queue_drain_deferred_wlh + 288 (queue.c:7193)\n37 libdispatch.dylib \t0x000000019ca29bd8 _dispatch_workloop_worker_thread + 540 (queue.c:6787)\n38 libsystem_pthread.dylib \t0x000000021fa1c680 _pthread_wqthread + 288 (pthread.c:2696)\n39 libsystem_pthread.dylib \t0x000000021fa1a474 start_wqthread + 8 (:-1)\n\nThread 2:\n0 libsystem_pthread.dylib \t0x000000021fa1a46c start_wqthread + 0 (:-1)\n\nThread 3 name:\nThread 3:\n0 libsystem_kernel.dylib \t0x00000001e63b657c __getdirentries64 + 8 (:-1)\n1 libsystem_c.dylib \t0x000000019ca5bc84 _readdir_unlocked + 220 (readdir.c:97)\n2 libsystem_c.dylib \t0x000000019ca5bb8c readdir + 44 (readdir.c:137)\n3 Foundation \t0x00000001939d8d9c _POSIXDirectoryContentsSequence.Iterator.next() + 76 (FileOperations+Enumeration.swift:331)\n4 Foundation \t0x00000001939d88bc specialized _NSFileManagerBridge.contentsOfDirectory(atPath:) + 672 (FileManager+Bridge.swift:63)\n5 Foundation \t0x00000001939d85a4 @objc _NSFileManagerBridge.contentsOfDirectory(atPath:) + 76 (<compiler-generated>:62)\n6 DamusNotificationService \t0x0000000102469bd0 specialized closure #1 in DiskStorage.Backend.setupCacheChecking() + 360 (DiskStorage.swift:105)\n7 DamusNotificationService \t0x0000000102402bd4 <deduplicated_symbol> + 28\n8 libdispatch.dylib \t0x000000019ca15248 _dispatch_call_block_and_release + 32 (init.c:1549)\n9 libdispatch.dylib \t0x000000019ca16fa8 _dispatch_client_callout + 20 (object.m:576)\n10 libdispatch.dylib \t0x000000019ca29094 _dispatch_root_queue_drain + 860 (queue.c:7331)\n11 libdispatch.dylib \t0x000000019ca296c4 _dispatch_worker_thread2 + 156 (queue.c:7399)\n12 libsystem_pthread.dylib \t0x000000021fa1c644 _pthread_wqthread + 228 (pthread.c:2709)\n13 libsystem_pthread.dylib \t0x000000021fa1a474 start_wqthread + 8 (:-1)\n\nThread 4:\n0 libsystem_kernel.dylib \t0x00000001e63b6090 __psynch_cvwait + 8 (:-1)\n1 libsystem_pthread.dylib \t0x000000021fa1cf98 _pthread_cond_wait + 1204 (pthread_cond.c:862)\n2 DamusNotificationService \t0x00000001023e88b8 prot_queue_pop_all + 52 (protected_queue.h:190)\n3 DamusNotificationService \t0x00000001023e84d4 ndb_writer_thread + 120 (nostrdb.c:2709)\n4 libsystem_pthread.dylib \t0x000000021fa1a7d0 _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x000000021fa1a480 thread_start + 8 (:-1)\n\nThread 5:\n0 libsystem_kernel.dylib \t0x00000001e63b6090 __psynch_cvwait + 8 (:-1)\n1 libsystem_pthread.dylib \t0x000000021fa1cf98 _pthread_cond_wait + 1204 (pthread_cond.c:862)\n2 DamusNotificationService \t0x00000001023e88b8 prot_queue_pop_all + 52 (protected_queue.h:190)\n3 DamusNotificationService \t0x00000001023e92e8 ndb_ingester_thread + 156 (nostrdb.c:2809)\n4 libsystem_pthread.dylib \t0x000000021fa1a7d0 _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x000000021fa1a480 thread_start + 8 (:-1)\n\nThread 6:\n0 libsystem_kernel.dylib \t0x00000001e63b6090 __psynch_cvwait + 8 (:-1)\n1 libsystem_pthread.dylib \t0x000000021fa1cf98 _pthread_cond_wait + 1204 (pthread_cond.c:862)\n2 DamusNotificationService \t0x00000001023e88b8 prot_queue_pop_all + 52 (protected_queue.h:190)\n3 DamusNotificationService \t0x00000001023e92e8 ndb_ingester_thread + 156 (nostrdb.c:2809)\n4 libsystem_pthread.dylib \t0x000000021fa1a7d0 _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x000000021fa1a480 thread_start + 8 (:-1)\n\nThread 7:\n0 libsystem_kernel.dylib \t0x00000001e63b6090 __psynch_cvwait + 8 (:-1)\n1 libsystem_pthread.dylib \t0x000000021fa1cf98 _pthread_cond_wait + 1204 (pthread_cond.c:862)\n2 DamusNotificationService \t0x00000001023e88b8 prot_queue_pop_all + 52 (protected_queue.h:190)\n3 DamusNotificationService \t0x00000001023e92e8 ndb_ingester_thread + 156 (nostrdb.c:2809)\n4 libsystem_pthread.dylib \t0x000000021fa1a7d0 _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x000000021fa1a480 thread_start + 8 (:-1)\n\nThread 8:\n0 libsystem_kernel.dylib \t0x00000001e63b6090 __psynch_cvwait + 8 (:-1)\n1 libsystem_pthread.dylib \t0x000000021fa1cf98 _pthread_cond_wait + 1204 (pthread_cond.c:862)\n2 DamusNotificationService \t0x00000001023e88b8 prot_queue_pop_all + 52 (protected_queue.h:190)\n3 DamusNotificationService \t0x00000001023e92e8 ndb_ingester_thread + 156 (nostrdb.c:2809)\n4 libsystem_pthread.dylib \t0x000000021fa1a7d0 _pthread_start + 136 (pthread.c:931)\n5 libsystem_pthread.dylib \t0x000000021fa1a480 thread_start + 8 (:-1)\n\n\nThread 1 crashed with ARM Thread State (64-bit):\n x0: 0x0000000000000016 x1: 0x0000000000000009 x2: 0x000000016dbc8b88 x3: 0x000000016dbc8c10\n x4: 0x000000016dbc8c58 x5: 0x000000016dbc8c50 x6: 0x0000000000000001 x7: 0x0000000000000001\n x8: 0x0000000000000000 x9: 0x0000000103625a50 x10: 0x0000000000000003 x11: 0x0000000200000003\n x12: 0x000000000000000c x13: 0x00000001034ee260 x14: 0x00000001fd308d90 x15: 0x00000001fd308d90\n x16: 0x000000019360e150 x17: 0x00000001feb28b98 x18: 0x0000000000000000 x19: 0x000000016dbc8b88\n x20: 0x0000000116074000 x21: 0x0000000000000009 x22: 0x000000010343f4e0 x23: 0x40000001034658b0\n x24: 0x0000002000000000 x25: 0x000000010f45c040 x26: 0x0000000000000000 x27: 0x0000000103625a40\n x28: 0x0000000116074000 fp: 0x000000016dbc8b70 lr: 0x00000001023e4270\n sp: 0x000000016dbc8b40 pc: 0x00000001023e0fe8 cpsr: 0xa0000000\n esr: 0x92000006 (Data Abort) byte read Translation fault\n```",
+ "x": 1000,
+ "y": -82,
+ "width": 1225,
+ "height": 2442
+ }
+ ],
+ "edges": [
+ {
+ "id": "7921adc708cb3a7b",
+ "fromNode": "8ba807ac67740bc0",
+ "fromSide": "bottom",
+ "toNode": "c450e9832ffe7a3d",
+ "toSide": "top"
+ },
+ {
+ "id": "8cd54328766e3a74",
+ "fromNode": "b27baddab9517a38",
+ "fromSide": "bottom",
+ "toNode": "3d398a7223484f22",
+ "toSide": "top"
+ },
+ {
+ "id": "141570202b1594fa",
+ "fromNode": "c450e9832ffe7a3d",
+ "fromSide": "bottom",
+ "toNode": "906aae4882968fc5",
+ "toSide": "top"
+ },
+ {
+ "id": "c04c77ad678215f6",
+ "fromNode": "c450e9832ffe7a3d",
+ "fromSide": "bottom",
+ "toNode": "e619c24d803fc838",
+ "toSide": "top"
+ },
+ {
+ "id": "b1642de657a7849f",
+ "fromNode": "3d398a7223484f22",
+ "fromSide": "bottom",
+ "toNode": "915e462d7852f6bb",
+ "toSide": "top"
+ },
+ {
+ "id": "fa6c9971a72041f4",
+ "fromNode": "e48e5b32c0265d73",
+ "fromSide": "bottom",
+ "toNode": "4f83ab4aa30b00f1",
+ "toSide": "top"
+ },
+ {
+ "id": "0b9d1757cec27068",
+ "fromNode": "4f83ab4aa30b00f1",
+ "fromSide": "bottom",
+ "toNode": "0df74aca0192124e",
+ "toSide": "top"
+ },
+ {
+ "id": "fcf498e888535135",
+ "fromNode": "d275d9d3cca27202",
+ "fromSide": "bottom",
+ "toNode": "c04db70a0e694555",
+ "toSide": "top"
+ },
+ {
+ "id": "ac60f0136f93b0da",
+ "fromNode": "e182bb08633f0c8e",
+ "fromSide": "bottom",
+ "toNode": "39b704e37669fc83",
+ "toSide": "top"
+ },
+ {
+ "id": "a5963df395dff2cb",
+ "fromNode": "c04db70a0e694555",
+ "fromSide": "bottom",
+ "toNode": "a7b2dd98e8fb8b98",
+ "toSide": "top"
+ },
+ {
+ "id": "f18aa25135d3bf72",
+ "fromNode": "39b704e37669fc83",
+ "fromSide": "bottom",
+ "toNode": "a3718eb8e2f9f28d",
+ "toSide": "top"
+ }
+ ]
+}
diff --git a/crates/notedeck_notebook/src/debug.rs b/crates/notedeck_notebook/src/debug.rs
@@ -0,0 +1,26 @@
+
+/*
+fn debug_slider(
+ ui: &mut egui::Ui,
+ id: egui::Id,
+ point: Pos2,
+ initial: f32,
+ range: std::ops::RangeInclusive<f32>,
+) -> f32 {
+ let mut val = ui.data_mut(|d| *d.get_temp_mut_or::<f32>(id, initial));
+ let nudge = vec2(10.0, 10.0);
+ let slider = Rect::from_min_max(point - nudge, point + nudge);
+ let label = Rect::from_min_max(point + nudge * 2.0, point - nudge * 2.0);
+
+ let old_val = val;
+ ui.put(slider, egui::Slider::new(&mut val, range));
+ ui.put(label, egui::Label::new(format!("{val}")));
+
+ if val != old_val {
+ ui.data_mut(|d| d.insert_temp(id, val))
+ }
+
+ val
+}
+*/
+
diff --git a/crates/notedeck_notebook/src/lib.rs b/crates/notedeck_notebook/src/lib.rs
@@ -0,0 +1,60 @@
+use crate::ui::{edge_ui, node_ui};
+use egui::{Pos2, Rect};
+use jsoncanvas::JsonCanvas;
+use notedeck::{AppAction, AppContext};
+
+mod ui;
+
+pub struct Notebook {
+ canvas: JsonCanvas,
+ scene_rect: Rect,
+ loaded: bool,
+}
+
+impl Notebook {
+ pub fn new() -> Self {
+ Notebook::default()
+ }
+}
+
+impl Default for Notebook {
+ fn default() -> Self {
+ Notebook {
+ canvas: demo_canvas(),
+ scene_rect: Rect::from_min_max(Pos2::ZERO, Pos2::ZERO),
+ loaded: false,
+ }
+ }
+}
+
+impl notedeck::App for Notebook {
+ fn update(&mut self, _ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
+ //let app_action: Option<AppAction> = None;
+
+ if !self.loaded {
+ self.scene_rect = ui.available_rect_before_wrap();
+ self.loaded = true;
+ }
+
+ egui::Scene::new().show(ui, &mut self.scene_rect, |ui| {
+ // render nodes
+ for (_node_id, node) in self.canvas.get_nodes().iter() {
+ let _resp = node_ui(ui, node);
+ }
+
+ // render edges
+ for (_edge_id, edge) in self.canvas.get_edges().iter() {
+ let _resp = edge_ui(ui, self.canvas.get_nodes(), edge);
+ }
+ });
+
+ None
+ }
+}
+
+fn demo_canvas() -> JsonCanvas {
+ let demo_json: String = include_str!("../demo.canvas").to_string();
+
+ let canvas: JsonCanvas = demo_json.parse().unwrap_or_else(|_| JsonCanvas::default());
+ canvas
+}
diff --git a/crates/notedeck_notebook/src/ui.rs b/crates/notedeck_notebook/src/ui.rs
@@ -0,0 +1,184 @@
+use egui::{Align, Label, Pos2, Rect, Shape, Stroke, TextWrapMode, epaint::CubicBezierShape, vec2};
+use jsoncanvas::{
+ FileNode, GroupNode, LinkNode, Node, NodeId, TextNode,
+ edge::{Edge, Side},
+ node::GenericNode,
+};
+use std::collections::HashMap;
+use std::ops::Neg;
+
+fn node_rect(node: &GenericNode) -> Rect {
+ let x = node.x as f32;
+ let y = node.y as f32;
+ let width = node.width as f32;
+ let height = node.height as f32;
+
+ let min = Pos2::new(x, y);
+ let max = Pos2::new(x + width, y + height);
+
+ Rect::from_min_max(min, max)
+}
+
+fn side_point(side: &Side, node: &GenericNode) -> Pos2 {
+ let rect = node_rect(node);
+
+ match side {
+ Side::Top => rect.center_top(),
+ Side::Left => rect.left_center(),
+ Side::Right => rect.right_center(),
+ Side::Bottom => rect.center_bottom(),
+ }
+}
+
+/// a unit vector pointing outward from the given side
+fn side_tangent(side: &Side) -> egui::Vec2 {
+ match side {
+ Side::Top => vec2(0.0, -1.0),
+ Side::Bottom => vec2(0.0, 1.0),
+ Side::Left => vec2(-1.0, 0.0),
+ Side::Right => vec2(1.0, 0.0),
+ }
+}
+
+pub fn edge_ui(
+ ui: &mut egui::Ui,
+ nodes: &HashMap<NodeId, Node>,
+ edge: &Edge,
+) -> Option<egui::Response> {
+ let from_node = nodes.get(edge.from_node())?;
+ let to_node = nodes.get(edge.to_node())?;
+ let to_side = edge.to_side()?;
+ let from_side = edge.from_side()?;
+
+ // anchor from-side
+ let p0 = side_point(from_side, from_node.node());
+
+ // anchor b
+ let to_anchor = side_point(to_side, to_node.node());
+
+ // to-point is slightly offset to accomidate arrow
+ let p3 = to_anchor + side_tangent(to_side) * 2.0;
+
+ // bend debug
+ //let bend = debug_slider(ui, ui.id().with("bend"), p3, 0.25, 0.0..=1.0);
+ let bend = 0.28;
+
+ // How far to pull the tangents.
+ // ¼ of the distance between anchors feels very “Obsidian”.
+ let d = (p3 - p0).length() * bend;
+
+ // c1 = anchor A + (outward tangent) * d
+ let c1 = p0 + side_tangent(from_side) * d;
+
+ // c2 = anchor B + (inward tangent) * d
+ let c2 = p3 - side_tangent(to_side).neg() * d;
+
+ let color = ui.visuals().noninteractive().bg_stroke.color;
+ let stroke = egui::Stroke::new(4.0, color);
+ let bezier = CubicBezierShape::from_points_stroke([p0, c1, c2, p3], false, color, stroke);
+
+ ui.painter().add(Shape::CubicBezier(bezier));
+ arrow_ui(ui, to_side, to_anchor, color);
+
+ None
+}
+
+/// Paint a tiny triangular “arrow”.
+///
+/// * `ui` – the egui `Ui` you’re painting in
+/// * `side` – which edge of the box we’re attaching to
+/// * `point` – the exact spot on that edge the arrow’s tip should touch
+/// * `fill` – colour to fill the arrow with (usually your popup’s background)
+pub fn arrow_ui(ui: &mut egui::Ui, side: &Side, point: Pos2, fill: egui::Color32) {
+ let len: f32 = 12.0; // distance from tip to base
+ let width: f32 = 16.0; // length of the base
+ let stroke: f32 = 1.0; // length of the base
+
+ let verts = match side {
+ Side::Top => [
+ point, // tip
+ Pos2::new(point.x - width * 0.5, point.y - len), // base‑left (above)
+ Pos2::new(point.x + width * 0.5, point.y - len), // base‑right (above)
+ ],
+ Side::Bottom => [
+ point,
+ Pos2::new(point.x + width * 0.5, point.y + len), // below
+ Pos2::new(point.x - width * 0.5, point.y + len),
+ ],
+ Side::Left => [
+ point,
+ Pos2::new(point.x - len, point.y + width * 0.5), // left
+ Pos2::new(point.x - len, point.y - width * 0.5),
+ ],
+ Side::Right => [
+ point,
+ Pos2::new(point.x + len, point.y - width * 0.5), // right
+ Pos2::new(point.x + len, point.y + width * 0.5),
+ ],
+ };
+
+ ui.painter().add(egui::Shape::convex_polygon(
+ verts.to_vec(),
+ fill,
+ Stroke::new(stroke, fill), // add a stroke here if you want an outline
+ ));
+}
+
+pub fn node_ui(ui: &mut egui::Ui, node: &Node) -> egui::Response {
+ match node {
+ Node::Text(text_node) => text_node_ui(ui, text_node),
+ Node::File(file_node) => file_node_ui(ui, file_node),
+ Node::Link(link_node) => link_node_ui(ui, link_node),
+ Node::Group(group_node) => group_node_ui(ui, group_node),
+ }
+}
+
+fn text_node_ui(ui: &mut egui::Ui, node: &TextNode) -> egui::Response {
+ node_box_ui(ui, node.node(), |ui| {
+ egui::ScrollArea::vertical()
+ .show(ui, |ui| {
+ ui.with_layout(egui::Layout::left_to_right(Align::Min), |ui| {
+ ui.add(Label::new(node.text()).wrap_mode(TextWrapMode::Wrap))
+ })
+ })
+ .inner
+ .response
+ })
+}
+
+fn file_node_ui(ui: &mut egui::Ui, node: &FileNode) -> egui::Response {
+ node_box_ui(ui, node.node(), |ui| ui.label("file node"))
+}
+
+fn link_node_ui(ui: &mut egui::Ui, node: &LinkNode) -> egui::Response {
+ node_box_ui(ui, node.node(), |ui| ui.label("link node"))
+}
+
+fn group_node_ui(ui: &mut egui::Ui, node: &GroupNode) -> egui::Response {
+ node_box_ui(ui, node.node(), |ui| ui.label("group node"))
+}
+
+fn node_box_ui(
+ ui: &mut egui::Ui,
+ node: &GenericNode,
+ contents: impl FnOnce(&mut egui::Ui) -> egui::Response,
+) -> egui::Response {
+ let pos = node_rect(node);
+
+ ui.put(pos, |ui: &mut egui::Ui| {
+ egui::Frame::default()
+ .fill(ui.visuals().noninteractive().weak_bg_fill)
+ .inner_margin(egui::Margin::same(16))
+ .corner_radius(egui::CornerRadius::same(10))
+ .stroke(egui::Stroke::new(
+ 2.0,
+ ui.visuals().noninteractive().bg_stroke.color,
+ ))
+ .show(ui, |ui| {
+ let rect = ui.available_rect_before_wrap();
+ ui.allocate_at_least(ui.available_size(), egui::Sense::click());
+ ui.put(rect, contents);
+ })
+ .response
+ })
+}