From ec755493d96f31aa8c371488a20d2b89a7fc1353 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Wed, 11 Dec 2024 04:22:05 -0800 Subject: [PATCH] Introducing Damus Notedeck: a nostr browser This splits notedeck into: - notedeck - notedeck_chrome - notedeck_columns The `notedeck` crate is the library that `notedeck_chrome` and `notedeck_columns`, use. It contains common functionality related to notedeck apps such as the NoteCache, ImageCache, etc. The `notedeck_chrome` crate is the binary and ui chrome. It is responsible for managing themes, user accounts, signing, data paths, nostrdb, image caches etc. It will eventually have its own ui which has yet to be determined. For now it just manages the browser data, which is passed to apps via a new struct called `AppContext`. `notedeck_columns` is our columns app, with less responsibility now that more things are handled by `notedeck_chrome` There is still much work left to do before this is a proper browser: - process isolation - sandboxing - etc This is the beginning of a new era! We're just getting started. Signed-off-by: William Casarin --- .gitignore | 2 +- Cargo.lock | 50 +- Cargo.toml | 7 +- Makefile | 3 + .../assets => assets}/Logo-Gradient-2x.png | Bin .../Welcome to Nostrdeck 2x.png | Bin .../assets => assets}/app_icon.icns | Bin .../assets => assets}/damus-app-icon.png | Bin .../assets => assets}/damus-app-icon.svg | 0 .../assets => assets}/damus.ico | Bin .../assets => assets}/damus.svg | 0 .../assets => assets}/damus_rounded.svg | 0 .../assets => assets}/damus_rounded_80.png | Bin .../assets => assets}/favicon.ico | Bin .../fonts/DejaVuSans-Bold-SansEmoji.ttf | Bin .../fonts/DejaVuSans-Bold.ttf | Bin .../assets => assets}/fonts/DejaVuSans.ttf | Bin .../fonts/DejaVuSansSansEmoji.ttf | Bin .../fonts/Inconsolata-Regular.ttf | Bin .../fonts/NotoEmoji-Regular.ttf | Bin .../fonts/NotoSansCJK-Regular.ttc | Bin .../fonts/NotoSansThai-Regular.ttf | Bin .../ark/ark-pixel-10px-proportional-latin.ttf | Bin .../fonts/onest/OnestBlack1602-hint.ttf | Bin .../fonts/onest/OnestBold1602-hint.ttf | Bin .../fonts/onest/OnestExtraBold1602-hint.ttf | Bin .../fonts/onest/OnestLight1602-hint.ttf | Bin .../fonts/onest/OnestMedium1602-hint.ttf | Bin .../fonts/onest/OnestRegular1602-hint.ttf | Bin .../fonts/onest/OnestThin1602-hint.ttf | Bin .../fonts/pressstart/PressStart2P.ttf | Bin .../icons/add_account_icon_4x.png | Bin .../icons/add_column_dark_4x.png | Bin .../icons/add_column_light_4x.png | Bin .../icons/column_delete_icon_4x.png | Bin .../icons/column_delete_icon_light_4x.png | Bin .../icons/connected_icon_4x.png | Bin .../icons/connecting_icon_4x.png | Bin .../icons/delete_icon_4x.png | Bin .../icons/disconnected_icon_4x.png | Bin .../icons/help_icon_dark_4x.png | Bin .../icons/help_icon_inverted_4x.png | Bin .../icons/home_icon_dark_4x.png | Bin .../icons/new_deck_icon_4x_dark.png | Bin .../icons/notifications_icon_dark_4x.png | Bin .../assets => assets}/icons/plus_icon_4x.png | Bin .../assets => assets}/icons/reply-dark.png | Bin .../assets => assets}/icons/reply.png | Bin .../assets => assets}/icons/reply.svg | 0 .../icons/repost_icon_4x.png | Bin .../icons/repost_light_4x.png | Bin .../icons/select_icon_3x.png | Bin .../icons/settings_dark_4x.png | Bin .../icons/settings_light_4x.png | Bin .../icons/signout_icon_4x.png | Bin .../icons/universe_icon_dark_4x.png | Bin .../assets => assets}/manifest.json | 0 .../notedeck_columns/assets => assets}/sw.js | 0 crates/enostr/Cargo.toml | 5 +- crates/enostr/src/error.rs | 79 +-- crates/enostr/src/relay/message.rs | 1 - crates/notedeck/Cargo.toml | 86 +-- crates/notedeck/src/accounts.rs | 569 +++++++++++++++++ crates/notedeck/src/app.rs | 5 + crates/notedeck/src/args.rs | 111 ++++ crates/notedeck/src/context.rs | 19 + crates/notedeck/src/error.rs | 64 ++ .../src/filter.rs | 0 crates/notedeck/src/fonts.rs | 58 ++ .../src/imgcache.rs | 15 + crates/notedeck/src/lib.rs | 44 ++ .../src/muted.rs | 0 .../src/note.rs | 0 .../src/notecache.rs | 3 +- crates/notedeck/src/notedeck.rs | 105 ---- .../src/result.rs | 2 +- .../src/storage/file_key_storage.rs | 45 +- .../src/storage/file_storage.rs | 20 +- .../src/storage/key_storage_impl.rs | 28 +- crates/notedeck/src/storage/mod.rs | 11 + .../storage/security_framework_key_storage.rs | 12 +- crates/notedeck/src/style.rs | 46 ++ crates/notedeck/src/theme.rs | 101 +++ .../src/theme_handler.rs | 4 +- .../src/time.rs | 0 .../src/timecache.rs | 0 crates/notedeck/src/ui.rs | 24 + crates/notedeck/src/unknowns.rs | 356 +++++++++++ .../src/user_account.rs | 0 crates/notedeck_chrome/Cargo.toml | 82 +++ .../src/app_size.rs} | 4 +- .../src/fonts.rs | 72 +-- crates/notedeck_chrome/src/lib.rs | 4 + crates/notedeck_chrome/src/notedeck.rs | 392 ++++++++++++ .../src/preview.rs | 18 +- crates/notedeck_chrome/src/setup.rs | 79 +++ crates/notedeck_chrome/src/theme.rs | 132 ++++ crates/notedeck_columns/Cargo.toml | 5 +- crates/notedeck_columns/src/accounts/mod.rs | 578 +----------------- crates/notedeck_columns/src/accounts/route.rs | 6 - crates/notedeck_columns/src/actionbar.rs | 7 +- crates/notedeck_columns/src/app.rs | 412 ++++--------- crates/notedeck_columns/src/app_creation.rs | 82 --- crates/notedeck_columns/src/app_style.rs | 208 +------ crates/notedeck_columns/src/args.rs | 179 +----- crates/notedeck_columns/src/colors.rs | 114 +--- crates/notedeck_columns/src/error.rs | 133 +--- crates/notedeck_columns/src/frame_history.rs | 2 + crates/notedeck_columns/src/images.rs | 9 +- crates/notedeck_columns/src/lib.rs | 12 - .../notedeck_columns/src/multi_subscriber.rs | 5 +- crates/notedeck_columns/src/nav.rs | 187 +++--- crates/notedeck_columns/src/notes_holder.rs | 5 +- crates/notedeck_columns/src/profile.rs | 8 +- crates/notedeck_columns/src/storage/decks.rs | 12 +- .../notedeck_columns/src/storage/migration.rs | 14 +- crates/notedeck_columns/src/storage/mod.rs | 10 - crates/notedeck_columns/src/support.rs | 2 +- crates/notedeck_columns/src/test_data.rs | 12 +- crates/notedeck_columns/src/thread.rs | 5 +- crates/notedeck_columns/src/timeline/kind.rs | 21 +- crates/notedeck_columns/src/timeline/mod.rs | 22 +- crates/notedeck_columns/src/timeline/route.rs | 6 +- .../src/ui/account_login_view.rs | 2 +- crates/notedeck_columns/src/ui/accounts.rs | 54 +- crates/notedeck_columns/src/ui/add_column.rs | 101 ++- .../notedeck_columns/src/ui/column/header.rs | 7 +- .../notedeck_columns/src/ui/configure_deck.rs | 16 +- crates/notedeck_columns/src/ui/mention.rs | 7 +- crates/notedeck_columns/src/ui/mod.rs | 25 - .../notedeck_columns/src/ui/note/contents.rs | 19 +- .../notedeck_columns/src/ui/note/context.rs | 3 +- crates/notedeck_columns/src/ui/note/mod.rs | 78 +-- crates/notedeck_columns/src/ui/note/post.rs | 4 +- .../src/ui/note/quote_repost.rs | 3 +- crates/notedeck_columns/src/ui/note/reply.rs | 4 +- crates/notedeck_columns/src/ui/profile/mod.rs | 6 +- .../src/ui/profile/picture.rs | 3 +- .../src/ui/profile/preview.rs | 28 +- crates/notedeck_columns/src/ui/relay.rs | 12 +- crates/notedeck_columns/src/ui/side_panel.rs | 95 +-- crates/notedeck_columns/src/ui/support.rs | 19 +- crates/notedeck_columns/src/ui/thread.rs | 6 +- crates/notedeck_columns/src/ui/timeline.rs | 6 +- crates/notedeck_columns/src/ui/username.rs | 2 +- crates/notedeck_columns/src/unknowns.rs | 400 +----------- 146 files changed, 2820 insertions(+), 2794 deletions(-) rename {crates/notedeck_columns/assets => assets}/Logo-Gradient-2x.png (100%) rename {crates/notedeck_columns/assets => assets}/Welcome to Nostrdeck 2x.png (100%) rename {crates/notedeck_columns/assets => assets}/app_icon.icns (100%) rename {crates/notedeck_columns/assets => assets}/damus-app-icon.png (100%) rename {crates/notedeck_columns/assets => assets}/damus-app-icon.svg (100%) rename {crates/notedeck_columns/assets => assets}/damus.ico (100%) rename {crates/notedeck_columns/assets => assets}/damus.svg (100%) rename {crates/notedeck_columns/assets => assets}/damus_rounded.svg (100%) rename {crates/notedeck_columns/assets => assets}/damus_rounded_80.png (100%) rename {crates/notedeck_columns/assets => assets}/favicon.ico (100%) rename {crates/notedeck_columns/assets => assets}/fonts/DejaVuSans-Bold-SansEmoji.ttf (100%) rename {crates/notedeck_columns/assets => assets}/fonts/DejaVuSans-Bold.ttf (100%) rename {crates/notedeck_columns/assets => assets}/fonts/DejaVuSans.ttf (100%) rename {crates/notedeck_columns/assets => assets}/fonts/DejaVuSansSansEmoji.ttf (100%) rename {crates/notedeck_columns/assets => assets}/fonts/Inconsolata-Regular.ttf (100%) rename {crates/notedeck_columns/assets => assets}/fonts/NotoEmoji-Regular.ttf (100%) rename {crates/notedeck_columns/assets => assets}/fonts/NotoSansCJK-Regular.ttc (100%) rename {crates/notedeck_columns/assets => assets}/fonts/NotoSansThai-Regular.ttf (100%) rename {crates/notedeck_columns/assets => assets}/fonts/ark/ark-pixel-10px-proportional-latin.ttf (100%) rename {crates/notedeck_columns/assets => assets}/fonts/onest/OnestBlack1602-hint.ttf (100%) rename {crates/notedeck_columns/assets => assets}/fonts/onest/OnestBold1602-hint.ttf (100%) rename {crates/notedeck_columns/assets => assets}/fonts/onest/OnestExtraBold1602-hint.ttf (100%) rename {crates/notedeck_columns/assets => assets}/fonts/onest/OnestLight1602-hint.ttf (100%) rename {crates/notedeck_columns/assets => assets}/fonts/onest/OnestMedium1602-hint.ttf (100%) rename {crates/notedeck_columns/assets => assets}/fonts/onest/OnestRegular1602-hint.ttf (100%) rename {crates/notedeck_columns/assets => assets}/fonts/onest/OnestThin1602-hint.ttf (100%) rename {crates/notedeck_columns/assets => assets}/fonts/pressstart/PressStart2P.ttf (100%) rename {crates/notedeck_columns/assets => assets}/icons/add_account_icon_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/add_column_dark_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/add_column_light_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/column_delete_icon_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/column_delete_icon_light_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/connected_icon_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/connecting_icon_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/delete_icon_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/disconnected_icon_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/help_icon_dark_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/help_icon_inverted_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/home_icon_dark_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/new_deck_icon_4x_dark.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/notifications_icon_dark_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/plus_icon_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/reply-dark.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/reply.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/reply.svg (100%) rename {crates/notedeck_columns/assets => assets}/icons/repost_icon_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/repost_light_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/select_icon_3x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/settings_dark_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/settings_light_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/signout_icon_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/icons/universe_icon_dark_4x.png (100%) rename {crates/notedeck_columns/assets => assets}/manifest.json (100%) rename {crates/notedeck_columns/assets => assets}/sw.js (100%) create mode 100644 crates/notedeck/src/accounts.rs create mode 100644 crates/notedeck/src/app.rs create mode 100644 crates/notedeck/src/args.rs create mode 100644 crates/notedeck/src/context.rs create mode 100644 crates/notedeck/src/error.rs rename crates/{notedeck_columns => notedeck}/src/filter.rs (100%) create mode 100644 crates/notedeck/src/fonts.rs rename crates/{notedeck_columns => notedeck}/src/imgcache.rs (75%) create mode 100644 crates/notedeck/src/lib.rs rename crates/{notedeck_columns => notedeck}/src/muted.rs (100%) rename crates/{notedeck_columns => notedeck}/src/note.rs (100%) rename crates/{notedeck_columns => notedeck}/src/notecache.rs (95%) delete mode 100644 crates/notedeck/src/notedeck.rs rename crates/{notedeck_columns => notedeck}/src/result.rs (67%) rename crates/{notedeck_columns => notedeck}/src/storage/file_key_storage.rs (76%) rename crates/{notedeck_columns => notedeck}/src/storage/file_storage.rs (94%) rename crates/{notedeck_columns => notedeck}/src/storage/key_storage_impl.rs (76%) create mode 100644 crates/notedeck/src/storage/mod.rs rename crates/{notedeck_columns => notedeck}/src/storage/security_framework_key_storage.rs (93%) create mode 100644 crates/notedeck/src/style.rs create mode 100644 crates/notedeck/src/theme.rs rename crates/{notedeck_columns => notedeck}/src/theme_handler.rs (95%) rename crates/{notedeck_columns => notedeck}/src/time.rs (100%) rename crates/{notedeck_columns => notedeck}/src/timecache.rs (100%) create mode 100644 crates/notedeck/src/ui.rs create mode 100644 crates/notedeck/src/unknowns.rs rename crates/{notedeck_columns => notedeck}/src/user_account.rs (100%) create mode 100644 crates/notedeck_chrome/Cargo.toml rename crates/{notedeck_columns/src/app_size_handler.rs => notedeck_chrome/src/app_size.rs} (96%) rename crates/{notedeck_columns => notedeck_chrome}/src/fonts.rs (65%) create mode 100644 crates/notedeck_chrome/src/lib.rs create mode 100644 crates/notedeck_chrome/src/notedeck.rs rename crates/{notedeck => notedeck_chrome}/src/preview.rs (83%) create mode 100644 crates/notedeck_chrome/src/setup.rs create mode 100644 crates/notedeck_chrome/src/theme.rs diff --git a/.gitignore b/.gitignore index 3bba4da..84da6cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -.build-result .buildcmd +build.log perf.data perf.data.old .privenv diff --git a/Cargo.lock b/Cargo.lock index 2805703..cd4c968 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1184,6 +1184,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "thiserror 2.0.6", "tracing", "url", ] @@ -2484,7 +2485,7 @@ dependencies = [ "cc", "flatbuffers", "libc", - "thiserror 2.0.3", + "thiserror 2.0.6", "tokio", "tracing", "tracing-subscriber", @@ -2492,13 +2493,47 @@ dependencies = [ [[package]] name = "notedeck" +version = "0.1.0" +dependencies = [ + "base32", + "dirs", + "egui", + "enostr", + "hex", + "image", + "nostrdb", + "poll-promise", + "puffin 0.19.1 (git+https://github.com/jb55/puffin?rev=70ff86d5503815219b01a009afd3669b7903a057)", + "security-framework", + "serde", + "serde_json", + "strum", + "strum_macros", + "tempfile", + "thiserror 2.0.6", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "notedeck_chrome" version = "0.2.0" dependencies = [ "android-activity 0.4.3", "android_logger", "eframe", + "egui", + "egui_extras", + "enostr", + "nostrdb", + "notedeck", "notedeck_columns", + "serde_json", + "strum", + "tempfile", "tokio", + "tracing", "tracing-appender", "tracing-subscriber", "winit", @@ -2508,7 +2543,6 @@ dependencies = [ name = "notedeck_columns" version = "0.2.0" dependencies = [ - "base32", "bitflags 2.6.0", "dirs", "eframe", @@ -2525,6 +2559,7 @@ dependencies = [ "indexmap", "log", "nostrdb", + "notedeck", "open", "poll-promise", "puffin 0.19.1 (git+https://github.com/jb55/puffin?rev=70ff86d5503815219b01a009afd3669b7903a057)", @@ -2537,6 +2572,7 @@ dependencies = [ "strum", "strum_macros", "tempfile", + "thiserror 2.0.6", "tokio", "tracing", "tracing-appender", @@ -4226,11 +4262,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.3" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" dependencies = [ - "thiserror-impl 2.0.3", + "thiserror-impl 2.0.6", ] [[package]] @@ -4246,9 +4282,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.3" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index bf5ce57..9c52622 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,8 @@ resolver = "2" members = [ "crates/notedeck", - "crates/notedeck_columns", # Replace with the name of your subcrate + "crates/notedeck_chrome", + "crates/notedeck_columns", "crates/enostr", ] @@ -28,6 +29,7 @@ log = "0.4.17" nostr = { version = "0.30.0" } nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "71154e4100775f6932ee517da4350c433ba14ec7" } notedeck = { path = "crates/notedeck" } +notedeck_chrome = { path = "crates/notedeck_chrome" } notedeck_columns = { path = "crates/notedeck_columns" } open = "5.3.0" poll-promise = { version = "0.3.0", features = ["tokio"] } @@ -39,13 +41,16 @@ serde_derive = "1" serde_json = "1.0.89" strum = "0.26" strum_macros = "0.26" +thiserror = "2.0.6" tokio = { version = "1.16", features = ["macros", "rt-multi-thread", "fs"] } tracing = "0.1.40" tracing-appender = "0.2.3" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tempfile = "3.13.0" url = "2.5.2" urlencoding = "2.1.3" uuid = { version = "1.10.0", features = ["v4"] } +security-framework = "2.11.0" [profile.small] inherits = 'release' diff --git a/Makefile b/Makefile index 5b2d86b..9e94a49 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,9 @@ all: cargo check +check: + cargo check + tags: fake find . -type d -name target -prune -o -type f -name '*.rs' -print | xargs ctags diff --git a/crates/notedeck_columns/assets/Logo-Gradient-2x.png b/assets/Logo-Gradient-2x.png similarity index 100% rename from crates/notedeck_columns/assets/Logo-Gradient-2x.png rename to assets/Logo-Gradient-2x.png diff --git a/crates/notedeck_columns/assets/Welcome to Nostrdeck 2x.png b/assets/Welcome to Nostrdeck 2x.png similarity index 100% rename from crates/notedeck_columns/assets/Welcome to Nostrdeck 2x.png rename to assets/Welcome to Nostrdeck 2x.png diff --git a/crates/notedeck_columns/assets/app_icon.icns b/assets/app_icon.icns similarity index 100% rename from crates/notedeck_columns/assets/app_icon.icns rename to assets/app_icon.icns diff --git a/crates/notedeck_columns/assets/damus-app-icon.png b/assets/damus-app-icon.png similarity index 100% rename from crates/notedeck_columns/assets/damus-app-icon.png rename to assets/damus-app-icon.png diff --git a/crates/notedeck_columns/assets/damus-app-icon.svg b/assets/damus-app-icon.svg similarity index 100% rename from crates/notedeck_columns/assets/damus-app-icon.svg rename to assets/damus-app-icon.svg diff --git a/crates/notedeck_columns/assets/damus.ico b/assets/damus.ico similarity index 100% rename from crates/notedeck_columns/assets/damus.ico rename to assets/damus.ico diff --git a/crates/notedeck_columns/assets/damus.svg b/assets/damus.svg similarity index 100% rename from crates/notedeck_columns/assets/damus.svg rename to assets/damus.svg diff --git a/crates/notedeck_columns/assets/damus_rounded.svg b/assets/damus_rounded.svg similarity index 100% rename from crates/notedeck_columns/assets/damus_rounded.svg rename to assets/damus_rounded.svg diff --git a/crates/notedeck_columns/assets/damus_rounded_80.png b/assets/damus_rounded_80.png similarity index 100% rename from crates/notedeck_columns/assets/damus_rounded_80.png rename to assets/damus_rounded_80.png diff --git a/crates/notedeck_columns/assets/favicon.ico b/assets/favicon.ico similarity index 100% rename from crates/notedeck_columns/assets/favicon.ico rename to assets/favicon.ico diff --git a/crates/notedeck_columns/assets/fonts/DejaVuSans-Bold-SansEmoji.ttf b/assets/fonts/DejaVuSans-Bold-SansEmoji.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/DejaVuSans-Bold-SansEmoji.ttf rename to assets/fonts/DejaVuSans-Bold-SansEmoji.ttf diff --git a/crates/notedeck_columns/assets/fonts/DejaVuSans-Bold.ttf b/assets/fonts/DejaVuSans-Bold.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/DejaVuSans-Bold.ttf rename to assets/fonts/DejaVuSans-Bold.ttf diff --git a/crates/notedeck_columns/assets/fonts/DejaVuSans.ttf b/assets/fonts/DejaVuSans.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/DejaVuSans.ttf rename to assets/fonts/DejaVuSans.ttf diff --git a/crates/notedeck_columns/assets/fonts/DejaVuSansSansEmoji.ttf b/assets/fonts/DejaVuSansSansEmoji.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/DejaVuSansSansEmoji.ttf rename to assets/fonts/DejaVuSansSansEmoji.ttf diff --git a/crates/notedeck_columns/assets/fonts/Inconsolata-Regular.ttf b/assets/fonts/Inconsolata-Regular.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/Inconsolata-Regular.ttf rename to assets/fonts/Inconsolata-Regular.ttf diff --git a/crates/notedeck_columns/assets/fonts/NotoEmoji-Regular.ttf b/assets/fonts/NotoEmoji-Regular.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/NotoEmoji-Regular.ttf rename to assets/fonts/NotoEmoji-Regular.ttf diff --git a/crates/notedeck_columns/assets/fonts/NotoSansCJK-Regular.ttc b/assets/fonts/NotoSansCJK-Regular.ttc similarity index 100% rename from crates/notedeck_columns/assets/fonts/NotoSansCJK-Regular.ttc rename to assets/fonts/NotoSansCJK-Regular.ttc diff --git a/crates/notedeck_columns/assets/fonts/NotoSansThai-Regular.ttf b/assets/fonts/NotoSansThai-Regular.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/NotoSansThai-Regular.ttf rename to assets/fonts/NotoSansThai-Regular.ttf diff --git a/crates/notedeck_columns/assets/fonts/ark/ark-pixel-10px-proportional-latin.ttf b/assets/fonts/ark/ark-pixel-10px-proportional-latin.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/ark/ark-pixel-10px-proportional-latin.ttf rename to assets/fonts/ark/ark-pixel-10px-proportional-latin.ttf diff --git a/crates/notedeck_columns/assets/fonts/onest/OnestBlack1602-hint.ttf b/assets/fonts/onest/OnestBlack1602-hint.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/onest/OnestBlack1602-hint.ttf rename to assets/fonts/onest/OnestBlack1602-hint.ttf diff --git a/crates/notedeck_columns/assets/fonts/onest/OnestBold1602-hint.ttf b/assets/fonts/onest/OnestBold1602-hint.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/onest/OnestBold1602-hint.ttf rename to assets/fonts/onest/OnestBold1602-hint.ttf diff --git a/crates/notedeck_columns/assets/fonts/onest/OnestExtraBold1602-hint.ttf b/assets/fonts/onest/OnestExtraBold1602-hint.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/onest/OnestExtraBold1602-hint.ttf rename to assets/fonts/onest/OnestExtraBold1602-hint.ttf diff --git a/crates/notedeck_columns/assets/fonts/onest/OnestLight1602-hint.ttf b/assets/fonts/onest/OnestLight1602-hint.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/onest/OnestLight1602-hint.ttf rename to assets/fonts/onest/OnestLight1602-hint.ttf diff --git a/crates/notedeck_columns/assets/fonts/onest/OnestMedium1602-hint.ttf b/assets/fonts/onest/OnestMedium1602-hint.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/onest/OnestMedium1602-hint.ttf rename to assets/fonts/onest/OnestMedium1602-hint.ttf diff --git a/crates/notedeck_columns/assets/fonts/onest/OnestRegular1602-hint.ttf b/assets/fonts/onest/OnestRegular1602-hint.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/onest/OnestRegular1602-hint.ttf rename to assets/fonts/onest/OnestRegular1602-hint.ttf diff --git a/crates/notedeck_columns/assets/fonts/onest/OnestThin1602-hint.ttf b/assets/fonts/onest/OnestThin1602-hint.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/onest/OnestThin1602-hint.ttf rename to assets/fonts/onest/OnestThin1602-hint.ttf diff --git a/crates/notedeck_columns/assets/fonts/pressstart/PressStart2P.ttf b/assets/fonts/pressstart/PressStart2P.ttf similarity index 100% rename from crates/notedeck_columns/assets/fonts/pressstart/PressStart2P.ttf rename to assets/fonts/pressstart/PressStart2P.ttf diff --git a/crates/notedeck_columns/assets/icons/add_account_icon_4x.png b/assets/icons/add_account_icon_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/add_account_icon_4x.png rename to assets/icons/add_account_icon_4x.png diff --git a/crates/notedeck_columns/assets/icons/add_column_dark_4x.png b/assets/icons/add_column_dark_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/add_column_dark_4x.png rename to assets/icons/add_column_dark_4x.png diff --git a/crates/notedeck_columns/assets/icons/add_column_light_4x.png b/assets/icons/add_column_light_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/add_column_light_4x.png rename to assets/icons/add_column_light_4x.png diff --git a/crates/notedeck_columns/assets/icons/column_delete_icon_4x.png b/assets/icons/column_delete_icon_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/column_delete_icon_4x.png rename to assets/icons/column_delete_icon_4x.png diff --git a/crates/notedeck_columns/assets/icons/column_delete_icon_light_4x.png b/assets/icons/column_delete_icon_light_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/column_delete_icon_light_4x.png rename to assets/icons/column_delete_icon_light_4x.png diff --git a/crates/notedeck_columns/assets/icons/connected_icon_4x.png b/assets/icons/connected_icon_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/connected_icon_4x.png rename to assets/icons/connected_icon_4x.png diff --git a/crates/notedeck_columns/assets/icons/connecting_icon_4x.png b/assets/icons/connecting_icon_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/connecting_icon_4x.png rename to assets/icons/connecting_icon_4x.png diff --git a/crates/notedeck_columns/assets/icons/delete_icon_4x.png b/assets/icons/delete_icon_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/delete_icon_4x.png rename to assets/icons/delete_icon_4x.png diff --git a/crates/notedeck_columns/assets/icons/disconnected_icon_4x.png b/assets/icons/disconnected_icon_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/disconnected_icon_4x.png rename to assets/icons/disconnected_icon_4x.png diff --git a/crates/notedeck_columns/assets/icons/help_icon_dark_4x.png b/assets/icons/help_icon_dark_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/help_icon_dark_4x.png rename to assets/icons/help_icon_dark_4x.png diff --git a/crates/notedeck_columns/assets/icons/help_icon_inverted_4x.png b/assets/icons/help_icon_inverted_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/help_icon_inverted_4x.png rename to assets/icons/help_icon_inverted_4x.png diff --git a/crates/notedeck_columns/assets/icons/home_icon_dark_4x.png b/assets/icons/home_icon_dark_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/home_icon_dark_4x.png rename to assets/icons/home_icon_dark_4x.png diff --git a/crates/notedeck_columns/assets/icons/new_deck_icon_4x_dark.png b/assets/icons/new_deck_icon_4x_dark.png similarity index 100% rename from crates/notedeck_columns/assets/icons/new_deck_icon_4x_dark.png rename to assets/icons/new_deck_icon_4x_dark.png diff --git a/crates/notedeck_columns/assets/icons/notifications_icon_dark_4x.png b/assets/icons/notifications_icon_dark_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/notifications_icon_dark_4x.png rename to assets/icons/notifications_icon_dark_4x.png diff --git a/crates/notedeck_columns/assets/icons/plus_icon_4x.png b/assets/icons/plus_icon_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/plus_icon_4x.png rename to assets/icons/plus_icon_4x.png diff --git a/crates/notedeck_columns/assets/icons/reply-dark.png b/assets/icons/reply-dark.png similarity index 100% rename from crates/notedeck_columns/assets/icons/reply-dark.png rename to assets/icons/reply-dark.png diff --git a/crates/notedeck_columns/assets/icons/reply.png b/assets/icons/reply.png similarity index 100% rename from crates/notedeck_columns/assets/icons/reply.png rename to assets/icons/reply.png diff --git a/crates/notedeck_columns/assets/icons/reply.svg b/assets/icons/reply.svg similarity index 100% rename from crates/notedeck_columns/assets/icons/reply.svg rename to assets/icons/reply.svg diff --git a/crates/notedeck_columns/assets/icons/repost_icon_4x.png b/assets/icons/repost_icon_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/repost_icon_4x.png rename to assets/icons/repost_icon_4x.png diff --git a/crates/notedeck_columns/assets/icons/repost_light_4x.png b/assets/icons/repost_light_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/repost_light_4x.png rename to assets/icons/repost_light_4x.png diff --git a/crates/notedeck_columns/assets/icons/select_icon_3x.png b/assets/icons/select_icon_3x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/select_icon_3x.png rename to assets/icons/select_icon_3x.png diff --git a/crates/notedeck_columns/assets/icons/settings_dark_4x.png b/assets/icons/settings_dark_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/settings_dark_4x.png rename to assets/icons/settings_dark_4x.png diff --git a/crates/notedeck_columns/assets/icons/settings_light_4x.png b/assets/icons/settings_light_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/settings_light_4x.png rename to assets/icons/settings_light_4x.png diff --git a/crates/notedeck_columns/assets/icons/signout_icon_4x.png b/assets/icons/signout_icon_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/signout_icon_4x.png rename to assets/icons/signout_icon_4x.png diff --git a/crates/notedeck_columns/assets/icons/universe_icon_dark_4x.png b/assets/icons/universe_icon_dark_4x.png similarity index 100% rename from crates/notedeck_columns/assets/icons/universe_icon_dark_4x.png rename to assets/icons/universe_icon_dark_4x.png diff --git a/crates/notedeck_columns/assets/manifest.json b/assets/manifest.json similarity index 100% rename from crates/notedeck_columns/assets/manifest.json rename to assets/manifest.json diff --git a/crates/notedeck_columns/assets/sw.js b/assets/sw.js similarity index 100% rename from crates/notedeck_columns/assets/sw.js rename to assets/sw.js diff --git a/crates/enostr/Cargo.toml b/crates/enostr/Cargo.toml index 3af81ba..45e8ccf 100644 --- a/crates/enostr/Cargo.toml +++ b/crates/enostr/Cargo.toml @@ -9,9 +9,10 @@ edition = "2021" ewebsock = { version = "0.2.0", features = ["tls"] } serde_derive = "1" serde = { version = "1", features = ["derive"] } # You only need this if you want app persistence -serde_json = "1.0.89" -nostr = { version = "0.30.0" } +serde_json = { workspace = true } +nostr = { workspace = true } nostrdb = { workspace = true } hex = { workspace = true } tracing = { workspace = true } +thiserror = { workspace = true } url = { workspace = true } diff --git a/crates/enostr/src/error.rs b/crates/enostr/src/error.rs index 9a1733a..e8e8a5a 100644 --- a/crates/enostr/src/error.rs +++ b/crates/enostr/src/error.rs @@ -1,38 +1,39 @@ //use nostr::prelude::secp256k1; use std::array::TryFromSliceError; -use std::fmt; +use thiserror::Error; -#[derive(Debug)] +#[derive(Error, Debug)] pub enum Error { + #[error("message is empty")] Empty, - DecodeFailed, - HexDecodeFailed, - InvalidBech32, - InvalidByteSize, - InvalidSignature, - InvalidPublicKey, - // Secp(secp256k1::Error), - Json(serde_json::Error), - Nostrdb(nostrdb::Error), - Generic(String), -} -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Empty => write!(f, "message is empty"), - Self::DecodeFailed => write!(f, "decoding failed"), - Self::InvalidSignature => write!(f, "invalid signature"), - Self::HexDecodeFailed => write!(f, "hex decoding failed"), - Self::InvalidByteSize => write!(f, "invalid byte size"), - Self::InvalidBech32 => write!(f, "invalid bech32 string"), - Self::InvalidPublicKey => write!(f, "invalid public key"), - //Self::Secp(e) => write!(f, "{e}"), - Self::Json(e) => write!(f, "{e}"), - Self::Nostrdb(e) => write!(f, "{e}"), - Self::Generic(e) => write!(f, "{e}"), - } - } + #[error("decoding failed")] + DecodeFailed, + + #[error("hex decoding failed")] + HexDecodeFailed, + + #[error("invalid bech32")] + InvalidBech32, + + #[error("invalid byte size")] + InvalidByteSize, + + #[error("invalid signature")] + InvalidSignature, + + #[error("invalid public key")] + InvalidPublicKey, + + // Secp(secp256k1::Error), + #[error("json error: {0}")] + Json(#[from] serde_json::Error), + + #[error("nostrdb error: {0}")] + Nostrdb(#[from] nostrdb::Error), + + #[error("{0}")] + Generic(String), } impl From for Error { @@ -52,23 +53,3 @@ impl From for Error { Error::HexDecodeFailed } } - -/* -impl From for Error { - fn from(e: secp256k1::Error) -> Self { - Error::Secp(e) - } -} -*/ - -impl From for Error { - fn from(e: serde_json::Error) -> Self { - Error::Json(e) - } -} - -impl From for Error { - fn from(e: nostrdb::Error) -> Self { - Error::Nostrdb(e) - } -} diff --git a/crates/enostr/src/relay/message.rs b/crates/enostr/src/relay/message.rs index 83f8c6a..a7dd12e 100644 --- a/crates/enostr/src/relay/message.rs +++ b/crates/enostr/src/relay/message.rs @@ -139,7 +139,6 @@ impl<'a> RelayMessage<'a> { #[cfg(test)] mod tests { use super::*; - use crate::Note; #[test] fn test_handle_valid_notice() -> Result<()> { diff --git a/crates/notedeck/Cargo.toml b/crates/notedeck/Cargo.toml index 962e787..31bdfba 100644 --- a/crates/notedeck/Cargo.toml +++ b/crates/notedeck/Cargo.toml @@ -1,71 +1,33 @@ [package] name = "notedeck" -version = "0.2.0" -authors = ["William Casarin ", "kernelkind "] +version = "0.1.0" edition = "2021" -default-run = "notedeck" -#rust-version = "1.60" -license = "GPLv3" -description = "A nostr browser" +description = "The APIs and data structures used by notedeck apps" [dependencies] -notedeck_columns = { workspace = true } -tracing-subscriber = { workspace = true } -tracing-appender = { workspace = true } -tokio = { workspace = true } -eframe = { workspace = true } +nostrdb = { workspace = true } +url = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } +dirs = { workspace = true } +enostr = { workspace = true } +egui = { workspace = true } +image = { workspace = true } +base32 = { workspace = true } +poll-promise = { workspace = true } +tracing = { workspace = true } +uuid = { workspace = true } +serde_json = { workspace = true } +serde = { workspace = true } +hex = { workspace = true } +thiserror = { workspace = true } +puffin = { workspace = true, optional = true } -[[bin]] -name = "notedeck" -path = "src/notedeck.rs" +[dev-dependencies] +tempfile = { workspace = true } -[[bin]] -name = "ui_preview" -path = "src/preview.rs" +[target.'cfg(target_os = "macos")'.dependencies] +security-framework = { workspace = true } [features] -default = [] -profiling = ["notedeck_columns/puffin"] - -[target.'cfg(target_os = "android")'.dependencies] -android_logger = "0.11.1" -android-activity = { version = "0.4", features = [ "native-activity" ] } -winit = { version = "0.30.5", features = [ "android-native-activity" ] } -#winit = { git="https://github.com/rust-windowing/winit.git", rev = "2a58b785fed2a3746f7c7eebce95bce67ddfd27c", features = ["android-native-activity"] } - -[package.metadata.bundle] -identifier = "com.damus.notedeck" -icon = ["assets/app_icon.icns"] - -[package.metadata.android] -package = "com.damus.app" -apk_name = "damus" -#assets = "assets" - -[[package.metadata.android.uses_feature]] -name = "android.hardware.vulkan.level" -required = true -version = 1 - -[[package.metadata.android.uses_permission]] -name = "android.permission.WRITE_EXTERNAL_STORAGE" -max_sdk_version = 18 - -[[package.metadata.android.uses_permission]] -name = "android.permission.READ_EXTERNAL_STORAGE" -max_sdk_version = 18 - -[package.metadata.android.signing.release] -path = "damus.keystore" -keystore_password = "damuskeystore" - -[[package.metadata.android.uses_permission]] -name = "android.permission.INTERNET" - -[package.metadata.android.application] -label = "Damus" - -[package.metadata.generate-rpm] -assets = [ - { source = "target/release/notedeck", dest = "/usr/bin/notedeck", mode = "755" }, -] +profiling = ["puffin"] diff --git a/crates/notedeck/src/accounts.rs b/crates/notedeck/src/accounts.rs new file mode 100644 index 0000000..03f125d --- /dev/null +++ b/crates/notedeck/src/accounts.rs @@ -0,0 +1,569 @@ +use tracing::{debug, error, info}; + +use crate::{ + KeyStorageResponse, KeyStorageType, Muted, SingleUnkIdAction, UnknownIds, UserAccount, +}; +use enostr::{ClientMessage, FilledKeypair, Keypair, RelayPool}; +use nostrdb::{Filter, Ndb, Note, NoteKey, Subscription, Transaction}; +use std::cmp::Ordering; +use std::collections::{BTreeMap, BTreeSet}; +use url::Url; +use uuid::Uuid; + +// TODO: remove this +use std::sync::Arc; + +#[derive(Debug)] +pub enum AccountsAction { + Switch(usize), + Remove(usize), +} + +pub struct AccountRelayData { + filter: Filter, + subid: String, + sub: Option, + local: BTreeSet, // used locally but not advertised + advertised: BTreeSet, // advertised via NIP-65 +} + +#[derive(Default)] +pub struct ContainsAccount { + pub has_nsec: bool, + pub index: usize, +} + +#[must_use = "You must call process_login_action on this to handle unknown ids"] +pub struct AddAccountAction { + pub accounts_action: Option, + pub unk_id_action: SingleUnkIdAction, +} + +impl AccountRelayData { + pub fn new(ndb: &Ndb, pool: &mut RelayPool, pubkey: &[u8; 32]) -> Self { + // Construct a filter for the user's NIP-65 relay list + let filter = Filter::new() + .authors([pubkey]) + .kinds([10002]) + .limit(1) + .build(); + + // Local ndb subscription + let ndbsub = ndb + .subscribe(&[filter.clone()]) + .expect("ndb relay list subscription"); + + // Query the ndb immediately to see if the user list is already there + let txn = Transaction::new(ndb).expect("transaction"); + let lim = filter.limit().unwrap_or(crate::filter::default_limit()) as i32; + let nks = ndb + .query(&txn, &[filter.clone()], lim) + .expect("query user relays results") + .iter() + .map(|qr| qr.note_key) + .collect::>(); + let relays = Self::harvest_nip65_relays(ndb, &txn, &nks); + debug!( + "pubkey {}: initial relays {:?}", + hex::encode(pubkey), + relays + ); + + // Id for future remote relay subscriptions + let subid = Uuid::new_v4().to_string(); + + // Add remote subscription to existing relays + pool.subscribe(subid.clone(), vec![filter.clone()]); + + AccountRelayData { + filter, + subid, + sub: Some(ndbsub), + local: BTreeSet::new(), + advertised: relays.into_iter().collect(), + } + } + + // standardize the format (ie, trailing slashes) to avoid dups + pub fn canonicalize_url(url: &str) -> String { + match Url::parse(url) { + Ok(parsed_url) => parsed_url.to_string(), + Err(_) => url.to_owned(), // If parsing fails, return the original URL. + } + } + + fn harvest_nip65_relays(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Vec { + let mut relays = Vec::new(); + for nk in nks.iter() { + if let Ok(note) = ndb.get_note_by_key(txn, *nk) { + for tag in note.tags() { + match tag.get(0).and_then(|t| t.variant().str()) { + Some("r") => { + if let Some(url) = tag.get(1).and_then(|f| f.variant().str()) { + relays.push(Self::canonicalize_url(url)); + } + } + Some("alt") => { + // ignore for now + } + Some(x) => { + error!("harvest_nip65_relays: unexpected tag type: {}", x); + } + None => { + error!("harvest_nip65_relays: invalid tag"); + } + } + } + } + } + relays + } +} + +pub struct AccountMutedData { + filter: Filter, + subid: String, + sub: Option, + muted: Arc, +} + +impl AccountMutedData { + pub fn new(ndb: &Ndb, pool: &mut RelayPool, pubkey: &[u8; 32]) -> Self { + // Construct a filter for the user's NIP-51 muted list + let filter = Filter::new() + .authors([pubkey]) + .kinds([10000]) + .limit(1) + .build(); + + // Local ndb subscription + let ndbsub = ndb + .subscribe(&[filter.clone()]) + .expect("ndb muted subscription"); + + // Query the ndb immediately to see if the user's muted list is already there + let txn = Transaction::new(ndb).expect("transaction"); + let lim = filter.limit().unwrap_or(crate::filter::default_limit()) as i32; + let nks = ndb + .query(&txn, &[filter.clone()], lim) + .expect("query user muted results") + .iter() + .map(|qr| qr.note_key) + .collect::>(); + let muted = Self::harvest_nip51_muted(ndb, &txn, &nks); + debug!("pubkey {}: initial muted {:?}", hex::encode(pubkey), muted); + + // Id for future remote relay subscriptions + let subid = Uuid::new_v4().to_string(); + + // Add remote subscription to existing relays + pool.subscribe(subid.clone(), vec![filter.clone()]); + + AccountMutedData { + filter, + subid, + sub: Some(ndbsub), + muted: Arc::new(muted), + } + } + + fn harvest_nip51_muted(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Muted { + let mut muted = Muted::default(); + for nk in nks.iter() { + if let Ok(note) = ndb.get_note_by_key(txn, *nk) { + for tag in note.tags() { + match tag.get(0).and_then(|t| t.variant().str()) { + Some("p") => { + if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) { + muted.pubkeys.insert(*id); + } + } + Some("t") => { + if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) { + muted.hashtags.insert(str.to_string()); + } + } + Some("word") => { + if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) { + muted.words.insert(str.to_string()); + } + } + Some("e") => { + if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) { + muted.threads.insert(*id); + } + } + Some("alt") => { + // maybe we can ignore these? + } + Some(x) => error!("query_nip51_muted: unexpected tag: {}", x), + None => error!( + "query_nip51_muted: bad tag value: {:?}", + tag.get_unchecked(0).variant() + ), + } + } + } + } + muted + } +} + +pub struct AccountData { + relay: AccountRelayData, + muted: AccountMutedData, +} + +/// The interface for managing the user's accounts. +/// Represents all user-facing operations related to account management. +pub struct Accounts { + currently_selected_account: Option, + accounts: Vec, + key_store: KeyStorageType, + account_data: BTreeMap<[u8; 32], AccountData>, + forced_relays: BTreeSet, + bootstrap_relays: BTreeSet, + needs_relay_config: bool, +} + +impl Accounts { + pub fn new(key_store: KeyStorageType, forced_relays: Vec) -> Self { + let accounts = if let KeyStorageResponse::ReceivedResult(res) = key_store.get_keys() { + res.unwrap_or_default() + } else { + Vec::new() + }; + + let currently_selected_account = get_selected_index(&accounts, &key_store); + let account_data = BTreeMap::new(); + let forced_relays: BTreeSet = forced_relays + .into_iter() + .map(|u| AccountRelayData::canonicalize_url(&u)) + .collect(); + let bootstrap_relays = [ + "wss://relay.damus.io", + // "wss://pyramid.fiatjaf.com", // Uncomment if needed + "wss://nos.lol", + "wss://nostr.wine", + "wss://purplepag.es", + ] + .iter() + .map(|&url| url.to_string()) + .map(|u| AccountRelayData::canonicalize_url(&u)) + .collect(); + + Accounts { + currently_selected_account, + accounts, + key_store, + account_data, + forced_relays, + bootstrap_relays, + needs_relay_config: true, + } + } + + pub fn get_accounts(&self) -> &Vec { + &self.accounts + } + + pub fn get_account(&self, ind: usize) -> Option<&UserAccount> { + self.accounts.get(ind) + } + + pub fn find_account(&self, pk: &[u8; 32]) -> Option<&UserAccount> { + self.accounts.iter().find(|acc| acc.pubkey.bytes() == pk) + } + + pub fn remove_account(&mut self, index: usize) { + if let Some(account) = self.accounts.get(index) { + let _ = self.key_store.remove_key(account); + self.accounts.remove(index); + + if let Some(selected_index) = self.currently_selected_account { + match selected_index.cmp(&index) { + Ordering::Greater => { + self.select_account(selected_index - 1); + } + Ordering::Equal => { + if self.accounts.is_empty() { + // If no accounts remain, clear the selection + self.clear_selected_account(); + } else if index >= self.accounts.len() { + // If the removed account was the last one, select the new last account + self.select_account(self.accounts.len() - 1); + } else { + // Otherwise, select the account at the same position + self.select_account(index); + } + } + Ordering::Less => {} + } + } + } + } + + fn contains_account(&self, pubkey: &[u8; 32]) -> Option { + for (index, account) in self.accounts.iter().enumerate() { + let has_pubkey = account.pubkey.bytes() == pubkey; + let has_nsec = account.secret_key.is_some(); + if has_pubkey { + return Some(ContainsAccount { has_nsec, index }); + } + } + + None + } + + #[must_use = "UnknownIdAction's must be handled. Use .process_unknown_id_action()"] + pub fn add_account(&mut self, account: Keypair) -> AddAccountAction { + let pubkey = account.pubkey; + let switch_to_index = if let Some(contains_acc) = self.contains_account(pubkey.bytes()) { + if account.secret_key.is_some() && !contains_acc.has_nsec { + info!( + "user provided nsec, but we already have npub {}. Upgrading to nsec", + pubkey + ); + let _ = self.key_store.add_key(&account); + + self.accounts[contains_acc.index] = account; + } else { + info!("already have account, not adding {}", pubkey); + } + contains_acc.index + } else { + info!("adding new account {}", pubkey); + let _ = self.key_store.add_key(&account); + self.accounts.push(account); + self.accounts.len() - 1 + }; + + AddAccountAction { + accounts_action: Some(AccountsAction::Switch(switch_to_index)), + unk_id_action: SingleUnkIdAction::pubkey(pubkey), + } + } + + pub fn num_accounts(&self) -> usize { + self.accounts.len() + } + + pub fn get_selected_account_index(&self) -> Option { + self.currently_selected_account + } + + pub fn selected_or_first_nsec(&self) -> Option> { + self.get_selected_account() + .and_then(|kp| kp.to_full()) + .or_else(|| self.accounts.iter().find_map(|a| a.to_full())) + } + + pub fn get_selected_account(&self) -> Option<&UserAccount> { + if let Some(account_index) = self.currently_selected_account { + if let Some(account) = self.get_account(account_index) { + Some(account) + } else { + None + } + } else { + None + } + } + + pub fn select_account(&mut self, index: usize) { + if let Some(account) = self.accounts.get(index) { + self.currently_selected_account = Some(index); + self.key_store.select_key(Some(account.pubkey)); + } + } + + pub fn clear_selected_account(&mut self) { + self.currently_selected_account = None; + self.key_store.select_key(None); + } + + pub fn mutefun(&self) -> Box bool> { + if let Some(index) = self.currently_selected_account { + if let Some(account) = self.accounts.get(index) { + let pubkey = account.pubkey.bytes(); + if let Some(account_data) = self.account_data.get(pubkey) { + let muted = Arc::clone(&account_data.muted.muted); + return Box::new(move |note: &Note| muted.is_muted(note)); + } + } + } + Box::new(|_: &Note| false) + } + + pub fn send_initial_filters(&mut self, pool: &mut RelayPool, relay_url: &str) { + for data in self.account_data.values() { + pool.send_to( + &ClientMessage::req(data.relay.subid.clone(), vec![data.relay.filter.clone()]), + relay_url, + ); + pool.send_to( + &ClientMessage::req(data.muted.subid.clone(), vec![data.muted.filter.clone()]), + relay_url, + ); + } + } + + // Returns added and removed accounts + fn delta_accounts(&self) -> (Vec<[u8; 32]>, Vec<[u8; 32]>) { + let mut added = Vec::new(); + for pubkey in self.accounts.iter().map(|a| a.pubkey.bytes()) { + if !self.account_data.contains_key(pubkey) { + added.push(*pubkey); + } + } + let mut removed = Vec::new(); + for pubkey in self.account_data.keys() { + if self.contains_account(pubkey).is_none() { + removed.push(*pubkey); + } + } + (added, removed) + } + + fn handle_added_account(&mut self, ndb: &Ndb, pool: &mut RelayPool, pubkey: &[u8; 32]) { + debug!("handle_added_account {}", hex::encode(pubkey)); + + // Create the user account data + let new_account_data = AccountData { + relay: AccountRelayData::new(ndb, pool, pubkey), + muted: AccountMutedData::new(ndb, pool, pubkey), + }; + self.account_data.insert(*pubkey, new_account_data); + } + + fn handle_removed_account(&mut self, pubkey: &[u8; 32]) { + debug!("handle_removed_account {}", hex::encode(pubkey)); + // FIXME - we need to unsubscribe here + self.account_data.remove(pubkey); + } + + fn poll_for_updates(&mut self, ndb: &Ndb) -> bool { + let mut changed = false; + for (pubkey, data) in &mut self.account_data { + if let Some(sub) = data.relay.sub { + let nks = ndb.poll_for_notes(sub, 1); + if !nks.is_empty() { + let txn = Transaction::new(ndb).expect("txn"); + let relays = AccountRelayData::harvest_nip65_relays(ndb, &txn, &nks); + debug!( + "pubkey {}: updated relays {:?}", + hex::encode(pubkey), + relays + ); + data.relay.advertised = relays.into_iter().collect(); + changed = true; + } + } + if let Some(sub) = data.muted.sub { + let nks = ndb.poll_for_notes(sub, 1); + if !nks.is_empty() { + let txn = Transaction::new(ndb).expect("txn"); + let muted = AccountMutedData::harvest_nip51_muted(ndb, &txn, &nks); + debug!("pubkey {}: updated muted {:?}", hex::encode(pubkey), muted); + data.muted.muted = Arc::new(muted); + changed = true; + } + } + } + changed + } + + fn update_relay_configuration( + &mut self, + pool: &mut RelayPool, + wakeup: impl Fn() + Send + Sync + Clone + 'static, + ) { + // If forced relays are set use them only + let mut desired_relays = self.forced_relays.clone(); + + // Compose the desired relay lists from the accounts + if desired_relays.is_empty() { + for data in self.account_data.values() { + desired_relays.extend(data.relay.local.iter().cloned()); + desired_relays.extend(data.relay.advertised.iter().cloned()); + } + } + + // If no relays are specified at this point use the bootstrap list + if desired_relays.is_empty() { + desired_relays = self.bootstrap_relays.clone(); + } + + debug!("current relays: {:?}", pool.urls()); + debug!("desired relays: {:?}", desired_relays); + + let add: BTreeSet = desired_relays.difference(&pool.urls()).cloned().collect(); + let sub: BTreeSet = pool.urls().difference(&desired_relays).cloned().collect(); + if !add.is_empty() { + debug!("configuring added relays: {:?}", add); + let _ = pool.add_urls(add, wakeup); + } + if !sub.is_empty() { + debug!("removing unwanted relays: {:?}", sub); + pool.remove_urls(&sub); + } + + debug!("current relays: {:?}", pool.urls()); + } + + pub fn update(&mut self, ndb: &Ndb, pool: &mut RelayPool, ctx: &egui::Context) { + // IMPORTANT - This function is called in the UI update loop, + // make sure it is fast when idle + + // On the initial update the relays need config even if nothing changes below + let mut relays_changed = self.needs_relay_config; + + let ctx2 = ctx.clone(); + let wakeup = move || { + ctx2.request_repaint(); + }; + + // Were any accounts added or removed? + let (added, removed) = self.delta_accounts(); + for pk in added { + self.handle_added_account(ndb, pool, &pk); + relays_changed = true; + } + for pk in removed { + self.handle_removed_account(&pk); + relays_changed = true; + } + + // Did any accounts receive updates (ie NIP-65 relay lists) + relays_changed = self.poll_for_updates(ndb) || relays_changed; + + // If needed, update the relay configuration + if relays_changed { + self.update_relay_configuration(pool, wakeup); + self.needs_relay_config = false; + } + } +} + +fn get_selected_index(accounts: &[UserAccount], keystore: &KeyStorageType) -> Option { + match keystore.get_selected_key() { + KeyStorageResponse::ReceivedResult(Ok(Some(pubkey))) => { + return accounts.iter().position(|account| account.pubkey == pubkey); + } + + KeyStorageResponse::ReceivedResult(Err(e)) => error!("Error getting selected key: {}", e), + KeyStorageResponse::Waiting | KeyStorageResponse::ReceivedResult(Ok(None)) => {} + }; + + None +} + +impl AddAccountAction { + // Simple wrapper around processing the unknown action to expose too + // much internal logic. This allows us to have a must_use on our + // LoginAction type, otherwise the SingleUnkIdAction's must_use will + // be lost when returned in the login action + pub fn process_action(&mut self, ids: &mut UnknownIds, ndb: &Ndb, txn: &Transaction) { + self.unk_id_action.process_action(ids, ndb, txn); + } +} diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs new file mode 100644 index 0000000..cc7d3b2 --- /dev/null +++ b/crates/notedeck/src/app.rs @@ -0,0 +1,5 @@ +use crate::AppContext; + +pub trait App { + fn update(&mut self, ctx: &mut AppContext<'_>); +} diff --git a/crates/notedeck/src/args.rs b/crates/notedeck/src/args.rs new file mode 100644 index 0000000..ed265e1 --- /dev/null +++ b/crates/notedeck/src/args.rs @@ -0,0 +1,111 @@ +use enostr::{Keypair, Pubkey, SecretKey}; +use tracing::error; + +pub struct Args { + pub relays: Vec, + pub is_mobile: Option, + pub keys: Vec, + pub light: bool, + pub debug: bool, + pub use_keystore: bool, + pub dbpath: Option, + pub datapath: Option, +} + +impl Args { + pub fn parse(args: &[String]) -> Self { + let mut res = Args { + relays: vec![], + is_mobile: None, + keys: vec![], + light: false, + debug: false, + use_keystore: true, + dbpath: None, + datapath: None, + }; + + let mut i = 0; + let len = args.len(); + while i < len { + let arg = &args[i]; + + if arg == "--mobile" { + res.is_mobile = Some(true); + } else if arg == "--light" { + res.light = true; + } else if arg == "--dark" { + res.light = false; + } else if arg == "--debug" { + res.debug = true; + } else if arg == "--pub" || arg == "--npub" { + i += 1; + let pubstr = if let Some(next_arg) = args.get(i) { + next_arg + } else { + error!("sec argument missing?"); + continue; + }; + + if let Ok(pk) = Pubkey::parse(pubstr) { + res.keys.push(Keypair::only_pubkey(pk)); + } else { + error!( + "failed to parse {} argument. Make sure to use hex or npub.", + arg + ); + } + } else if arg == "--sec" || arg == "--nsec" { + i += 1; + let secstr = if let Some(next_arg) = args.get(i) { + next_arg + } else { + error!("sec argument missing?"); + continue; + }; + + if let Ok(sec) = SecretKey::parse(secstr) { + res.keys.push(Keypair::from_secret(sec)); + } else { + error!( + "failed to parse {} argument. Make sure to use hex or nsec.", + arg + ); + } + } else if arg == "--dbpath" { + i += 1; + let path = if let Some(next_arg) = args.get(i) { + next_arg + } else { + error!("dbpath argument missing?"); + continue; + }; + res.dbpath = Some(path.clone()); + } else if arg == "--datapath" { + i += 1; + let path = if let Some(next_arg) = args.get(i) { + next_arg + } else { + error!("datapath argument missing?"); + continue; + }; + res.datapath = Some(path.clone()); + } else if arg == "-r" || arg == "--relay" { + i += 1; + let relay = if let Some(next_arg) = args.get(i) { + next_arg + } else { + error!("relay argument missing?"); + continue; + }; + res.relays.push(relay.clone()); + } else if arg == "--no-keystore" { + res.use_keystore = false; + } + + i += 1; + } + + res + } +} diff --git a/crates/notedeck/src/context.rs b/crates/notedeck/src/context.rs new file mode 100644 index 0000000..0280bf8 --- /dev/null +++ b/crates/notedeck/src/context.rs @@ -0,0 +1,19 @@ +use crate::{Accounts, Args, DataPath, ImageCache, NoteCache, ThemeHandler, UnknownIds}; + +use enostr::RelayPool; +use nostrdb::Ndb; + +// TODO: make this interface more sandboxed + +pub struct AppContext<'a> { + pub ndb: &'a Ndb, + pub img_cache: &'a mut ImageCache, + pub unknown_ids: &'a mut UnknownIds, + pub pool: &'a mut RelayPool, + pub note_cache: &'a mut NoteCache, + pub accounts: &'a mut Accounts, + pub path: &'a DataPath, + pub args: &'a Args, + pub theme: &'a mut ThemeHandler, + pub egui: &'a egui::Context, +} diff --git a/crates/notedeck/src/error.rs b/crates/notedeck/src/error.rs new file mode 100644 index 0000000..282bf2d --- /dev/null +++ b/crates/notedeck/src/error.rs @@ -0,0 +1,64 @@ +use std::io; + +/// App related errors +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("image error: {0}")] + Image(#[from] image::error::ImageError), + + #[error("io error: {0}")] + Io(#[from] io::Error), + + #[error("subscription error: {0}")] + SubscriptionError(SubscriptionError), + + #[error("filter error: {0}")] + Filter(FilterError), + + #[error("json error: {0}")] + Json(#[from] serde_json::Error), + + #[error("io error: {0}")] + Nostrdb(#[from] nostrdb::Error), + + #[error("generic error: {0}")] + Generic(String), +} + +impl From for Error { + fn from(s: String) -> Self { + Error::Generic(s) + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)] +pub enum FilterError { + #[error("empty contact list")] + EmptyContactList, +} + +#[derive(Debug, Eq, PartialEq, Copy, Clone, thiserror::Error)] +pub enum SubscriptionError { + #[error("no active subscriptions")] + NoActive, + + /// When a timeline has an unexpected number + /// of active subscriptions. Should only happen if there + /// is a bug in notedeck + #[error("unexpected subscription count")] + UnexpectedSubscriptionCount(i32), +} + +impl Error { + pub fn unexpected_sub_count(c: i32) -> Self { + Error::SubscriptionError(SubscriptionError::UnexpectedSubscriptionCount(c)) + } + + pub fn no_active_sub() -> Self { + Error::SubscriptionError(SubscriptionError::NoActive) + } + + pub fn empty_contact_list() -> Self { + Error::Filter(FilterError::EmptyContactList) + } +} diff --git a/crates/notedeck_columns/src/filter.rs b/crates/notedeck/src/filter.rs similarity index 100% rename from crates/notedeck_columns/src/filter.rs rename to crates/notedeck/src/filter.rs diff --git a/crates/notedeck/src/fonts.rs b/crates/notedeck/src/fonts.rs new file mode 100644 index 0000000..a226c1b --- /dev/null +++ b/crates/notedeck/src/fonts.rs @@ -0,0 +1,58 @@ +use crate::{ui, NotedeckTextStyle}; + +pub enum NamedFontFamily { + Medium, + Bold, + Emoji, +} + +impl NamedFontFamily { + pub fn as_str(&mut self) -> &'static str { + match self { + Self::Bold => "bold", + Self::Medium => "medium", + Self::Emoji => "emoji", + } + } + + pub fn as_family(&mut self) -> egui::FontFamily { + egui::FontFamily::Name(self.as_str().into()) + } +} + +pub fn desktop_font_size(text_style: &NotedeckTextStyle) -> f32 { + match text_style { + NotedeckTextStyle::Heading => 48.0, + NotedeckTextStyle::Heading2 => 24.0, + NotedeckTextStyle::Heading3 => 20.0, + NotedeckTextStyle::Heading4 => 14.0, + NotedeckTextStyle::Body => 16.0, + NotedeckTextStyle::Monospace => 13.0, + NotedeckTextStyle::Button => 13.0, + NotedeckTextStyle::Small => 12.0, + NotedeckTextStyle::Tiny => 10.0, + } +} + +pub fn mobile_font_size(text_style: &NotedeckTextStyle) -> f32 { + // TODO: tweak text sizes for optimal mobile viewing + match text_style { + NotedeckTextStyle::Heading => 48.0, + NotedeckTextStyle::Heading2 => 24.0, + NotedeckTextStyle::Heading3 => 20.0, + NotedeckTextStyle::Heading4 => 14.0, + NotedeckTextStyle::Body => 13.0, + NotedeckTextStyle::Monospace => 13.0, + NotedeckTextStyle::Button => 13.0, + NotedeckTextStyle::Small => 12.0, + NotedeckTextStyle::Tiny => 10.0, + } +} + +pub fn get_font_size(ctx: &egui::Context, text_style: &NotedeckTextStyle) -> f32 { + if ui::is_narrow(ctx) { + mobile_font_size(text_style) + } else { + desktop_font_size(text_style) + } +} diff --git a/crates/notedeck_columns/src/imgcache.rs b/crates/notedeck/src/imgcache.rs similarity index 75% rename from crates/notedeck_columns/src/imgcache.rs rename to crates/notedeck/src/imgcache.rs index 5916ac6..4a78fc0 100644 --- a/crates/notedeck_columns/src/imgcache.rs +++ b/crates/notedeck/src/imgcache.rs @@ -29,6 +29,21 @@ impl ImageCache { "img" } + /* + pub fn fetch(image: &str) -> Result { + let m_cached_promise = img_cache.map().get(image); + if m_cached_promise.is_none() { + let res = crate::images::fetch_img( + img_cache, + ui.ctx(), + &image, + ImageType::Content(width.round() as u32, height.round() as u32), + ); + img_cache.map_mut().insert(image.to_owned(), res); + } + } + */ + pub fn write(cache_dir: &path::Path, url: &str, data: ColorImage) -> Result<()> { let file_path = cache_dir.join(Self::key(url)); let file = File::options() diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs new file mode 100644 index 0000000..50ad2c7 --- /dev/null +++ b/crates/notedeck/src/lib.rs @@ -0,0 +1,44 @@ +mod accounts; +mod app; +mod args; +mod context; +mod error; +pub mod filter; +pub mod fonts; +mod imgcache; +mod muted; +pub mod note; +mod notecache; +mod result; +pub mod storage; +mod style; +pub mod theme; +mod theme_handler; +mod time; +mod timecache; +pub mod ui; +mod unknowns; +mod user_account; + +pub use accounts::{AccountData, Accounts, AccountsAction, AddAccountAction}; +pub use app::App; +pub use args::Args; +pub use context::AppContext; +pub use error::{Error, FilterError}; +pub use filter::{FilterState, FilterStates, UnifiedSubscription}; +pub use fonts::NamedFontFamily; +pub use imgcache::ImageCache; +pub use muted::{MuteFun, Muted}; +pub use note::NoteRef; +pub use notecache::{CachedNote, NoteCache}; +pub use result::Result; +pub use storage::{ + DataPath, DataPathType, Directory, FileKeyStorage, KeyStorageResponse, KeyStorageType, +}; +pub use style::NotedeckTextStyle; +pub use theme::ColorTheme; +pub use theme_handler::ThemeHandler; +pub use time::time_ago_since; +pub use timecache::TimeCached; +pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds}; +pub use user_account::UserAccount; diff --git a/crates/notedeck_columns/src/muted.rs b/crates/notedeck/src/muted.rs similarity index 100% rename from crates/notedeck_columns/src/muted.rs rename to crates/notedeck/src/muted.rs diff --git a/crates/notedeck_columns/src/note.rs b/crates/notedeck/src/note.rs similarity index 100% rename from crates/notedeck_columns/src/note.rs rename to crates/notedeck/src/note.rs diff --git a/crates/notedeck_columns/src/notecache.rs b/crates/notedeck/src/notecache.rs similarity index 95% rename from crates/notedeck_columns/src/notecache.rs rename to crates/notedeck/src/notecache.rs index 51bdc75..f46cdba 100644 --- a/crates/notedeck_columns/src/notecache.rs +++ b/crates/notedeck/src/notecache.rs @@ -1,5 +1,4 @@ -use crate::time::time_ago_since; -use crate::timecache::TimeCached; +use crate::{time_ago_since, TimeCached}; use nostrdb::{Note, NoteKey, NoteReply, NoteReplyBuf}; use std::collections::HashMap; use std::time::Duration; diff --git a/crates/notedeck/src/notedeck.rs b/crates/notedeck/src/notedeck.rs deleted file mode 100644 index 9350045..0000000 --- a/crates/notedeck/src/notedeck.rs +++ /dev/null @@ -1,105 +0,0 @@ -#![warn(clippy::all, rust_2018_idioms)] -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release -use notedeck_columns::{ - app_creation::generate_native_options, - storage::{DataPath, DataPathType}, - Damus, -}; -use std::{path::PathBuf, str::FromStr}; - -use tracing_subscriber::EnvFilter; - -// Entry point for wasm -//#[cfg(target_arch = "wasm32")] -//use wasm_bindgen::prelude::*; - -fn setup_logging(path: &DataPath) { - #[allow(unused_variables)] // need guard to live for lifetime of program - let (maybe_non_blocking, maybe_guard) = { - let log_path = path.path(DataPathType::Log); - // Setup logging to file - - use tracing_appender::{ - non_blocking, - rolling::{RollingFileAppender, Rotation}, - }; - - let file_appender = RollingFileAppender::new( - Rotation::DAILY, - log_path, - format!("notedeck-{}.log", env!("CARGO_PKG_VERSION")), - ); - - let (non_blocking, _guard) = non_blocking(file_appender); - - (Some(non_blocking), Some(_guard)) - }; - - // Log to stdout (if you run with `RUST_LOG=debug`). - if let Some(non_blocking_writer) = maybe_non_blocking { - use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt}; - - let console_layer = fmt::layer().with_target(true).with_writer(std::io::stdout); - - // Create the file layer (writes to the file) - let file_layer = fmt::layer() - .with_ansi(false) - .with_writer(non_blocking_writer); - - let env_filter = - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("notedeck=info")); - - // Set up the subscriber to combine both layers - tracing_subscriber::registry() - .with(console_layer) - .with(file_layer) - .with(env_filter) - .init(); - } else { - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .init(); - } -} - -// Desktop -#[cfg(not(target_arch = "wasm32"))] -#[tokio::main] -async fn main() { - let base_path = DataPath::default_base().unwrap_or(PathBuf::from_str(".").unwrap()); - let path = DataPath::new(&base_path); - - setup_logging(&path); - - let _res = eframe::run_native( - "Damus Notedeck", - generate_native_options(path), - Box::new(|cc| { - Ok(Box::new(Damus::new( - &cc.egui_ctx, - base_path, - std::env::args().collect(), - ))) - }), - ); -} - -#[cfg(target_arch = "wasm32")] -pub fn main() { - // Make sure panics are logged using `console.error`. - console_error_panic_hook::set_once(); - - // Redirect tracing to console.log and friends: - tracing_wasm::set_as_global_default(); - - wasm_bindgen_futures::spawn_local(async { - let web_options = eframe::WebOptions::default(); - eframe::start_web( - "the_canvas_id", // hardcode it - web_options, - Box::new(|cc| Box::new(Damus::new(cc, "."))), - ) - .await - .expect("failed to start eframe"); - }); -} diff --git a/crates/notedeck_columns/src/result.rs b/crates/notedeck/src/result.rs similarity index 67% rename from crates/notedeck_columns/src/result.rs rename to crates/notedeck/src/result.rs index 8207680..b465ded 100644 --- a/crates/notedeck_columns/src/result.rs +++ b/crates/notedeck/src/result.rs @@ -1,3 +1,3 @@ -use crate::error::Error; +use crate::Error; pub type Result = std::result::Result; diff --git a/crates/notedeck_columns/src/storage/file_key_storage.rs b/crates/notedeck/src/storage/file_key_storage.rs similarity index 76% rename from crates/notedeck_columns/src/storage/file_key_storage.rs rename to crates/notedeck/src/storage/file_key_storage.rs index e7a2ed8..9e5a9e5 100644 --- a/crates/notedeck_columns/src/storage/file_key_storage.rs +++ b/crates/notedeck/src/storage/file_key_storage.rs @@ -1,11 +1,9 @@ -use eframe::Result; +use crate::Result; use enostr::{Keypair, Pubkey, SerializableKeypair}; -use crate::Error; - use super::{ file_storage::{delete_file, write_file, Directory}, - key_storage_impl::{KeyStorageError, KeyStorageResponse}, + key_storage_impl::KeyStorageResponse, }; static SELECTED_PUBKEY_FILE_NAME: &str = "selected_pubkey"; @@ -25,21 +23,18 @@ impl FileKeyStorage { } } - fn add_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> { + fn add_key_internal(&self, key: &Keypair) -> Result<()> { write_file( &self.keys_directory.file_path, key.pubkey.hex(), - &serde_json::to_string(&SerializableKeypair::from_keypair(key, "", 7)) - .map_err(|e| KeyStorageError::Addition(Error::Generic(e.to_string())))?, + &serde_json::to_string(&SerializableKeypair::from_keypair(key, "", 7))?, ) - .map_err(KeyStorageError::Addition) } - fn get_keys_internal(&self) -> Result, KeyStorageError> { + fn get_keys_internal(&self) -> Result> { let keys = self .keys_directory - .get_files() - .map_err(KeyStorageError::Retrieval)? + .get_files()? .values() .filter_map(|str_key| serde_json::from_str::(str_key).ok()) .map(|serializable_keypair| serializable_keypair.to_keypair("")) @@ -47,41 +42,35 @@ impl FileKeyStorage { Ok(keys) } - fn remove_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> { + fn remove_key_internal(&self, key: &Keypair) -> Result<()> { delete_file(&self.keys_directory.file_path, key.pubkey.hex()) - .map_err(KeyStorageError::Removal) } - fn get_selected_pubkey(&self) -> Result, KeyStorageError> { + fn get_selected_pubkey(&self) -> Result> { let pubkey_str = self .selected_key_directory - .get_file(SELECTED_PUBKEY_FILE_NAME.to_owned()) - .map_err(KeyStorageError::Selection)?; + .get_file(SELECTED_PUBKEY_FILE_NAME.to_owned())?; - serde_json::from_str(&pubkey_str) - .map_err(|e| KeyStorageError::Selection(Error::Generic(e.to_string()))) + Ok(serde_json::from_str(&pubkey_str)?) } - fn select_pubkey(&self, pubkey: Option) -> Result<(), KeyStorageError> { + fn select_pubkey(&self, pubkey: Option) -> Result<()> { if let Some(pubkey) = pubkey { write_file( &self.selected_key_directory.file_path, SELECTED_PUBKEY_FILE_NAME.to_owned(), - &serde_json::to_string(&pubkey.hex()) - .map_err(|e| KeyStorageError::Selection(Error::Generic(e.to_string())))?, + &serde_json::to_string(&pubkey.hex())?, ) - .map_err(KeyStorageError::Selection) } else if self .selected_key_directory .get_file(SELECTED_PUBKEY_FILE_NAME.to_owned()) .is_ok() { // Case where user chose to have no selected pubkey, but one already exists - delete_file( + Ok(delete_file( &self.selected_key_directory.file_path, SELECTED_PUBKEY_FILE_NAME.to_owned(), - ) - .map_err(KeyStorageError::Selection) + )?) } else { Ok(()) } @@ -114,13 +103,15 @@ impl FileKeyStorage { mod tests { use std::path::PathBuf; + use super::Result; use super::*; + use enostr::Keypair; - static CREATE_TMP_DIR: fn() -> Result = + static CREATE_TMP_DIR: fn() -> Result = || Ok(tempfile::TempDir::new()?.path().to_path_buf()); impl FileKeyStorage { - fn mock() -> Result { + fn mock() -> Result { Ok(Self { keys_directory: Directory::new(CREATE_TMP_DIR()?), selected_key_directory: Directory::new(CREATE_TMP_DIR()?), diff --git a/crates/notedeck_columns/src/storage/file_storage.rs b/crates/notedeck/src/storage/file_storage.rs similarity index 94% rename from crates/notedeck_columns/src/storage/file_storage.rs rename to crates/notedeck/src/storage/file_storage.rs index 753075d..b3181a5 100644 --- a/crates/notedeck_columns/src/storage/file_storage.rs +++ b/crates/notedeck/src/storage/file_storage.rs @@ -6,7 +6,7 @@ use std::{ time::SystemTime, }; -use crate::Error; +use crate::{Error, Result}; #[derive(Debug, Clone)] pub struct DataPath { @@ -61,7 +61,7 @@ impl Directory { } /// Get the files in the current directory where the key is the file name and the value is the file contents - pub fn get_files(&self) -> Result, Error> { + pub fn get_files(&self) -> Result> { let dir = fs::read_dir(self.file_path.clone())?; let map = dir .filter_map(|f| f.ok()) @@ -76,7 +76,7 @@ impl Directory { Ok(map) } - pub fn get_file_names(&self) -> Result, Error> { + pub fn get_file_names(&self) -> Result> { let dir = fs::read_dir(self.file_path.clone())?; let names = dir .filter_map(|f| f.ok()) @@ -87,7 +87,7 @@ impl Directory { Ok(names) } - pub fn get_file(&self, file_name: String) -> Result { + pub fn get_file(&self, file_name: String) -> Result { let filepath = self.file_path.clone().join(file_name.clone()); if filepath.exists() && filepath.is_file() { @@ -103,7 +103,7 @@ impl Directory { } } - pub fn get_file_last_n_lines(&self, file_name: String, n: usize) -> Result { + pub fn get_file_last_n_lines(&self, file_name: String, n: usize) -> Result { let filepath = self.file_path.clone().join(file_name.clone()); if filepath.exists() && filepath.is_file() { @@ -140,7 +140,7 @@ impl Directory { } /// Get the file name which is most recently modified in the directory - pub fn get_most_recent(&self) -> Result, Error> { + pub fn get_most_recent(&self) -> Result> { let mut most_recent: Option<(SystemTime, String)> = None; for entry in fs::read_dir(&self.file_path)? { @@ -173,7 +173,7 @@ pub struct FileResult { } /// Write the file to the directory -pub fn write_file(directory: &Path, file_name: String, data: &str) -> Result<(), Error> { +pub fn write_file(directory: &Path, file_name: String, data: &str) -> Result<()> { if !directory.exists() { fs::create_dir_all(directory)? } @@ -182,7 +182,7 @@ pub fn write_file(directory: &Path, file_name: String, data: &str) -> Result<(), Ok(()) } -pub fn delete_file(directory: &Path, file_name: String) -> Result<(), Error> { +pub fn delete_file(directory: &Path, file_name: String) -> Result<()> { let file_to_delete = directory.join(file_name.clone()); if file_to_delete.exists() && file_to_delete.is_file() { fs::remove_file(file_to_delete).map_err(Error::Io) @@ -200,12 +200,12 @@ mod tests { use crate::{ storage::file_storage::{delete_file, write_file}, - Error, + Result, }; use super::Directory; - static CREATE_TMP_DIR: fn() -> Result = + static CREATE_TMP_DIR: fn() -> Result = || Ok(tempfile::TempDir::new()?.path().to_path_buf()); #[test] diff --git a/crates/notedeck_columns/src/storage/key_storage_impl.rs b/crates/notedeck/src/storage/key_storage_impl.rs similarity index 76% rename from crates/notedeck_columns/src/storage/key_storage_impl.rs rename to crates/notedeck/src/storage/key_storage_impl.rs index ae0cae0..6611689 100644 --- a/crates/notedeck_columns/src/storage/key_storage_impl.rs +++ b/crates/notedeck/src/storage/key_storage_impl.rs @@ -1,7 +1,7 @@ use enostr::{Keypair, Pubkey}; use super::file_key_storage::FileKeyStorage; -use crate::Error; +use crate::Result; #[cfg(target_os = "macos")] use super::security_framework_key_storage::SecurityFrameworkKeyStorage; @@ -18,7 +18,7 @@ pub enum KeyStorageType { #[derive(Debug)] pub enum KeyStorageResponse { Waiting, - ReceivedResult(Result), + ReceivedResult(Result), } impl PartialEq for KeyStorageResponse { @@ -86,27 +86,3 @@ impl KeyStorageType { } } } - -#[allow(dead_code)] -#[derive(Debug)] -pub enum KeyStorageError { - Retrieval(Error), - Addition(Error), - Selection(Error), - Removal(Error), - OSError(Error), -} - -impl std::fmt::Display for KeyStorageError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::Retrieval(e) => write!(f, "Failed to retrieve keys: {:?}", e), - Self::Addition(key) => write!(f, "Failed to add key: {:?}", key), - Self::Selection(pubkey) => write!(f, "Failed to select key: {:?}", pubkey), - Self::Removal(key) => write!(f, "Failed to remove key: {:?}", key), - Self::OSError(e) => write!(f, "OS had an error: {:?}", e), - } - } -} - -impl std::error::Error for KeyStorageError {} diff --git a/crates/notedeck/src/storage/mod.rs b/crates/notedeck/src/storage/mod.rs new file mode 100644 index 0000000..84ea2e0 --- /dev/null +++ b/crates/notedeck/src/storage/mod.rs @@ -0,0 +1,11 @@ +mod file_key_storage; +mod file_storage; + +pub use file_key_storage::FileKeyStorage; +pub use file_storage::{delete_file, write_file, DataPath, DataPathType, Directory}; + +#[cfg(target_os = "macos")] +mod security_framework_key_storage; + +pub mod key_storage_impl; +pub use key_storage_impl::{KeyStorageResponse, KeyStorageType}; diff --git a/crates/notedeck_columns/src/storage/security_framework_key_storage.rs b/crates/notedeck/src/storage/security_framework_key_storage.rs similarity index 93% rename from crates/notedeck_columns/src/storage/security_framework_key_storage.rs rename to crates/notedeck/src/storage/security_framework_key_storage.rs index 8827dee..0aece8f 100644 --- a/crates/notedeck_columns/src/storage/security_framework_key_storage.rs +++ b/crates/notedeck/src/storage/security_framework_key_storage.rs @@ -7,9 +7,9 @@ use security_framework::{ }; use tracing::error; -use crate::Error; +use crate::{Error, Result}; -use super::{key_storage_impl::KeyStorageError, KeyStorageResponse}; +use super::KeyStorageResponse; #[derive(Debug, PartialEq)] pub struct SecurityFrameworkKeyStorage { @@ -23,7 +23,7 @@ impl SecurityFrameworkKeyStorage { } } - fn add_key_internal(&self, key: &Keypair) -> Result<(), KeyStorageError> { + fn add_key_internal(&self, key: &Keypair) -> Result<()> { match set_generic_password( &self.service_name, key.pubkey.hex().as_str(), @@ -32,7 +32,7 @@ impl SecurityFrameworkKeyStorage { .map_or_else(|| &[] as &[u8], |sc| sc.as_secret_bytes()), ) { Ok(_) => Ok(()), - Err(e) => Err(KeyStorageError::Addition(Error::Generic(e.to_string()))), + Err(e) => Err(Error::Generic(e.to_string())), } } @@ -101,12 +101,12 @@ impl SecurityFrameworkKeyStorage { .collect() } - fn delete_key(&self, pubkey: &Pubkey) -> Result<(), KeyStorageError> { + fn delete_key(&self, pubkey: &Pubkey) -> Result<()> { match delete_generic_password(&self.service_name, pubkey.hex().as_str()) { Ok(_) => Ok(()), Err(e) => { error!("delete key error {}", e); - Err(KeyStorageError::Removal(Error::Generic(e.to_string()))) + Err(Error::Generic(e.to_string())) } } } diff --git a/crates/notedeck/src/style.rs b/crates/notedeck/src/style.rs new file mode 100644 index 0000000..160e3e6 --- /dev/null +++ b/crates/notedeck/src/style.rs @@ -0,0 +1,46 @@ +use egui::{FontFamily, TextStyle}; + +use strum_macros::EnumIter; + +#[derive(Copy, Clone, Eq, PartialEq, Debug, EnumIter)] +pub enum NotedeckTextStyle { + Heading, + Heading2, + Heading3, + Heading4, + Body, + Monospace, + Button, + Small, + Tiny, +} + +impl NotedeckTextStyle { + pub fn text_style(&self) -> TextStyle { + match self { + Self::Heading => TextStyle::Heading, + Self::Heading2 => TextStyle::Name("Heading2".into()), + Self::Heading3 => TextStyle::Name("Heading3".into()), + Self::Heading4 => TextStyle::Name("Heading4".into()), + Self::Body => TextStyle::Body, + Self::Monospace => TextStyle::Monospace, + Self::Button => TextStyle::Button, + Self::Small => TextStyle::Small, + Self::Tiny => TextStyle::Name("Tiny".into()), + } + } + + pub fn font_family(&self) -> FontFamily { + match self { + Self::Heading => FontFamily::Proportional, + Self::Heading2 => FontFamily::Proportional, + Self::Heading3 => FontFamily::Proportional, + Self::Heading4 => FontFamily::Proportional, + Self::Body => FontFamily::Proportional, + Self::Monospace => FontFamily::Monospace, + Self::Button => FontFamily::Proportional, + Self::Small => FontFamily::Proportional, + Self::Tiny => FontFamily::Proportional, + } + } +} diff --git a/crates/notedeck/src/theme.rs b/crates/notedeck/src/theme.rs new file mode 100644 index 0000000..cc9b510 --- /dev/null +++ b/crates/notedeck/src/theme.rs @@ -0,0 +1,101 @@ +use egui::{ + style::{Selection, WidgetVisuals, Widgets}, + Color32, Rounding, Shadow, Stroke, Visuals, +}; + +pub struct ColorTheme { + // VISUALS + pub panel_fill: Color32, + pub extreme_bg_color: Color32, + pub text_color: Color32, + pub err_fg_color: Color32, + pub warn_fg_color: Color32, + pub hyperlink_color: Color32, + pub selection_color: Color32, + + // WINDOW + pub window_fill: Color32, + pub window_stroke_color: Color32, + + // NONINTERACTIVE WIDGET + pub noninteractive_bg_fill: Color32, + pub noninteractive_weak_bg_fill: Color32, + pub noninteractive_bg_stroke_color: Color32, + pub noninteractive_fg_stroke_color: Color32, + + // INACTIVE WIDGET + pub inactive_bg_stroke_color: Color32, + pub inactive_bg_fill: Color32, + pub inactive_weak_bg_fill: Color32, +} + +const WIDGET_ROUNDING: Rounding = Rounding::same(8.0); + +pub fn create_themed_visuals(theme: ColorTheme, default: Visuals) -> Visuals { + Visuals { + hyperlink_color: theme.hyperlink_color, + override_text_color: Some(theme.text_color), + panel_fill: theme.panel_fill, + selection: Selection { + bg_fill: theme.selection_color, + stroke: Stroke { + width: 1.0, + color: theme.selection_color, + }, + }, + warn_fg_color: theme.warn_fg_color, + widgets: Widgets { + noninteractive: WidgetVisuals { + bg_fill: theme.noninteractive_bg_fill, + weak_bg_fill: theme.noninteractive_weak_bg_fill, + bg_stroke: Stroke { + width: 1.0, + color: theme.noninteractive_bg_stroke_color, + }, + fg_stroke: Stroke { + width: 1.0, + color: theme.noninteractive_fg_stroke_color, + }, + rounding: WIDGET_ROUNDING, + ..default.widgets.noninteractive + }, + inactive: WidgetVisuals { + bg_fill: theme.inactive_bg_fill, + weak_bg_fill: theme.inactive_weak_bg_fill, + bg_stroke: Stroke { + width: 1.0, + color: theme.inactive_bg_stroke_color, + }, + rounding: WIDGET_ROUNDING, + ..default.widgets.inactive + }, + hovered: WidgetVisuals { + rounding: WIDGET_ROUNDING, + ..default.widgets.hovered + }, + active: WidgetVisuals { + rounding: WIDGET_ROUNDING, + ..default.widgets.active + }, + open: WidgetVisuals { + ..default.widgets.open + }, + }, + extreme_bg_color: theme.extreme_bg_color, + error_fg_color: theme.err_fg_color, + window_rounding: Rounding::same(8.0), + window_fill: theme.window_fill, + window_shadow: Shadow { + offset: [0.0, 8.0].into(), + blur: 24.0, + spread: 0.0, + color: egui::Color32::from_rgba_unmultiplied(0x6D, 0x6D, 0x6D, 0x14), + }, + window_stroke: Stroke { + width: 1.0, + color: theme.window_stroke_color, + }, + image_loading_spinners: false, + ..default + } +} diff --git a/crates/notedeck_columns/src/theme_handler.rs b/crates/notedeck/src/theme_handler.rs similarity index 95% rename from crates/notedeck_columns/src/theme_handler.rs rename to crates/notedeck/src/theme_handler.rs index 0e3d529..90a0dca 100644 --- a/crates/notedeck_columns/src/theme_handler.rs +++ b/crates/notedeck/src/theme_handler.rs @@ -1,7 +1,7 @@ use egui::ThemePreference; use tracing::{error, info}; -use crate::storage::{write_file, DataPath, DataPathType, Directory}; +use crate::{storage, DataPath, DataPathType, Directory}; pub struct ThemeHandler { directory: Directory, @@ -43,7 +43,7 @@ impl ThemeHandler { } pub fn save(&self, theme: ThemePreference) { - match write_file( + match storage::write_file( &self.directory.file_path, THEME_FILE.to_owned(), &theme_to_serialized(&theme), diff --git a/crates/notedeck_columns/src/time.rs b/crates/notedeck/src/time.rs similarity index 100% rename from crates/notedeck_columns/src/time.rs rename to crates/notedeck/src/time.rs diff --git a/crates/notedeck_columns/src/timecache.rs b/crates/notedeck/src/timecache.rs similarity index 100% rename from crates/notedeck_columns/src/timecache.rs rename to crates/notedeck/src/timecache.rs diff --git a/crates/notedeck/src/ui.rs b/crates/notedeck/src/ui.rs new file mode 100644 index 0000000..9024e31 --- /dev/null +++ b/crates/notedeck/src/ui.rs @@ -0,0 +1,24 @@ +/// Determine if the screen is narrow. This is useful for detecting mobile +/// contexts, but with the nuance that we may also have a wide android tablet. +pub fn is_narrow(ctx: &egui::Context) -> bool { + let screen_size = ctx.input(|c| c.screen_rect().size()); + screen_size.x < 550.0 +} + +pub fn is_oled() -> bool { + is_compiled_as_mobile() +} + +#[inline] +#[allow(unreachable_code)] +pub fn is_compiled_as_mobile() -> bool { + #[cfg(any(target_os = "android", target_os = "ios"))] + { + true + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + false + } +} diff --git a/crates/notedeck/src/unknowns.rs b/crates/notedeck/src/unknowns.rs new file mode 100644 index 0000000..cdbdd4c --- /dev/null +++ b/crates/notedeck/src/unknowns.rs @@ -0,0 +1,356 @@ +use crate::{ + note::NoteRef, + notecache::{CachedNote, NoteCache}, + Result, +}; + +use enostr::{Filter, NoteId, Pubkey}; +use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction}; +use std::collections::HashSet; +use std::time::{Duration, Instant}; +use tracing::error; + +#[must_use = "process_action should be used on this result"] +pub enum SingleUnkIdAction { + NoAction, + NeedsProcess(UnknownId), +} + +#[must_use = "process_action should be used on this result"] +pub enum NoteRefsUnkIdAction { + NoAction, + NeedsProcess(Vec), +} + +impl NoteRefsUnkIdAction { + pub fn new(refs: Vec) -> Self { + NoteRefsUnkIdAction::NeedsProcess(refs) + } + + pub fn no_action() -> Self { + Self::NoAction + } + + pub fn process_action( + &self, + txn: &Transaction, + ndb: &Ndb, + unk_ids: &mut UnknownIds, + note_cache: &mut NoteCache, + ) { + match self { + Self::NoAction => {} + Self::NeedsProcess(refs) => { + UnknownIds::update_from_note_refs(txn, ndb, unk_ids, note_cache, refs); + } + } + } +} + +impl SingleUnkIdAction { + pub fn new(id: UnknownId) -> Self { + SingleUnkIdAction::NeedsProcess(id) + } + + pub fn no_action() -> Self { + Self::NoAction + } + + pub fn pubkey(pubkey: Pubkey) -> Self { + SingleUnkIdAction::new(UnknownId::Pubkey(pubkey)) + } + + pub fn note_id(note_id: NoteId) -> Self { + SingleUnkIdAction::new(UnknownId::Id(note_id)) + } + + /// Some functions may return unknown id actions that need to be processed. + /// For example, when we add a new account we need to make sure we have the + /// profile for that account. This function ensures we add this to the + /// unknown id tracker without adding side effects to functions. + pub fn process_action(&self, ids: &mut UnknownIds, ndb: &Ndb, txn: &Transaction) { + match self { + Self::NeedsProcess(id) => { + ids.add_unknown_id_if_missing(ndb, txn, id); + } + Self::NoAction => {} + } + } +} + +/// Unknown Id searcher +#[derive(Default)] +pub struct UnknownIds { + ids: HashSet, + first_updated: Option, + last_updated: Option, +} + +impl UnknownIds { + /// Simple debouncer + pub fn ready_to_send(&self) -> bool { + if self.ids.is_empty() { + return false; + } + + // we trigger on first set + if self.first_updated == self.last_updated { + return true; + } + + let last_updated = if let Some(last) = self.last_updated { + last + } else { + // if we've + return true; + }; + + Instant::now() - last_updated >= Duration::from_secs(2) + } + + pub fn ids(&self) -> &HashSet { + &self.ids + } + + pub fn ids_mut(&mut self) -> &mut HashSet { + &mut self.ids + } + + pub fn clear(&mut self) { + self.ids = HashSet::default(); + } + + pub fn filter(&self) -> Option> { + let ids: Vec<&UnknownId> = self.ids.iter().collect(); + get_unknown_ids_filter(&ids) + } + + /// We've updated some unknown ids, update the last_updated time to now + pub fn mark_updated(&mut self) { + let now = Instant::now(); + if self.first_updated.is_none() { + self.first_updated = Some(now); + } + self.last_updated = Some(now); + } + + pub fn update_from_note_key( + txn: &Transaction, + ndb: &Ndb, + unknown_ids: &mut UnknownIds, + note_cache: &mut NoteCache, + key: NoteKey, + ) -> bool { + let note = if let Ok(note) = ndb.get_note_by_key(txn, key) { + note + } else { + return false; + }; + + UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, ¬e) + } + + /// Should be called on freshly polled notes from subscriptions + pub fn update_from_note_refs( + txn: &Transaction, + ndb: &Ndb, + unknown_ids: &mut UnknownIds, + note_cache: &mut NoteCache, + note_refs: &[NoteRef], + ) { + for note_ref in note_refs { + Self::update_from_note_key(txn, ndb, unknown_ids, note_cache, note_ref.key); + } + } + + pub fn update_from_note( + txn: &Transaction, + ndb: &Ndb, + unknown_ids: &mut UnknownIds, + note_cache: &mut NoteCache, + note: &Note, + ) -> bool { + let before = unknown_ids.ids().len(); + let key = note.key().expect("note key"); + //let cached_note = note_cache.cached_note_or_insert(key, note).clone(); + let cached_note = note_cache.cached_note_or_insert(key, note); + if let Err(e) = get_unknown_note_ids(ndb, cached_note, txn, note, unknown_ids.ids_mut()) { + error!("UnknownIds::update_from_note {e}"); + } + let after = unknown_ids.ids().len(); + + if before != after { + unknown_ids.mark_updated(); + true + } else { + false + } + } + + pub fn add_unknown_id_if_missing(&mut self, ndb: &Ndb, txn: &Transaction, unk_id: &UnknownId) { + match unk_id { + UnknownId::Pubkey(pk) => self.add_pubkey_if_missing(ndb, txn, pk), + UnknownId::Id(note_id) => self.add_note_id_if_missing(ndb, txn, note_id), + } + } + + pub fn add_pubkey_if_missing(&mut self, ndb: &Ndb, txn: &Transaction, pubkey: &Pubkey) { + // we already have this profile, skip + if ndb.get_profile_by_pubkey(txn, pubkey).is_ok() { + return; + } + + self.ids.insert(UnknownId::Pubkey(*pubkey)); + self.mark_updated(); + } + + pub fn add_note_id_if_missing(&mut self, ndb: &Ndb, txn: &Transaction, note_id: &NoteId) { + // we already have this note, skip + if ndb.get_note_by_id(txn, note_id.bytes()).is_ok() { + return; + } + + self.ids.insert(UnknownId::Id(*note_id)); + self.mark_updated(); + } +} + +#[derive(Hash, Clone, Copy, PartialEq, Eq)] +pub enum UnknownId { + Pubkey(Pubkey), + Id(NoteId), +} + +impl UnknownId { + pub fn is_pubkey(&self) -> Option<&Pubkey> { + match self { + UnknownId::Pubkey(pk) => Some(pk), + _ => None, + } + } + + pub fn is_id(&self) -> Option<&NoteId> { + match self { + UnknownId::Id(id) => Some(id), + _ => None, + } + } +} + +/// Look for missing notes in various parts of notes that we see: +/// +/// - pubkeys and notes mentioned inside the note +/// - notes being replied to +/// +/// We return all of this in a HashSet so that we can fetch these from +/// remote relays. +/// +pub fn get_unknown_note_ids<'a>( + ndb: &Ndb, + cached_note: &CachedNote, + txn: &'a Transaction, + note: &Note<'a>, + ids: &mut HashSet, +) -> Result<()> { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + + // the author pubkey + if ndb.get_profile_by_pubkey(txn, note.pubkey()).is_err() { + ids.insert(UnknownId::Pubkey(Pubkey::new(*note.pubkey()))); + } + + // pull notes that notes are replying to + if cached_note.reply.root.is_some() { + let note_reply = cached_note.reply.borrow(note.tags()); + if let Some(root) = note_reply.root() { + if ndb.get_note_by_id(txn, root.id).is_err() { + ids.insert(UnknownId::Id(NoteId::new(*root.id))); + } + } + + if !note_reply.is_reply_to_root() { + if let Some(reply) = note_reply.reply() { + if ndb.get_note_by_id(txn, reply.id).is_err() { + ids.insert(UnknownId::Id(NoteId::new(*reply.id))); + } + } + } + } + + let blocks = ndb.get_blocks_by_key(txn, note.key().expect("note key"))?; + for block in blocks.iter(note) { + if block.blocktype() != BlockType::MentionBech32 { + continue; + } + + match block.as_mention().unwrap() { + Mention::Pubkey(npub) => { + if ndb.get_profile_by_pubkey(txn, npub.pubkey()).is_err() { + ids.insert(UnknownId::Pubkey(Pubkey::new(*npub.pubkey()))); + } + } + Mention::Profile(nprofile) => { + if ndb.get_profile_by_pubkey(txn, nprofile.pubkey()).is_err() { + ids.insert(UnknownId::Pubkey(Pubkey::new(*nprofile.pubkey()))); + } + } + Mention::Event(ev) => match ndb.get_note_by_id(txn, ev.id()) { + Err(_) => { + ids.insert(UnknownId::Id(NoteId::new(*ev.id()))); + if let Some(pk) = ev.pubkey() { + if ndb.get_profile_by_pubkey(txn, pk).is_err() { + ids.insert(UnknownId::Pubkey(Pubkey::new(*pk))); + } + } + } + Ok(note) => { + if ndb.get_profile_by_pubkey(txn, note.pubkey()).is_err() { + ids.insert(UnknownId::Pubkey(Pubkey::new(*note.pubkey()))); + } + } + }, + Mention::Note(note) => match ndb.get_note_by_id(txn, note.id()) { + Err(_) => { + ids.insert(UnknownId::Id(NoteId::new(*note.id()))); + } + Ok(note) => { + if ndb.get_profile_by_pubkey(txn, note.pubkey()).is_err() { + ids.insert(UnknownId::Pubkey(Pubkey::new(*note.pubkey()))); + } + } + }, + _ => {} + } + } + + Ok(()) +} + +fn get_unknown_ids_filter(ids: &[&UnknownId]) -> Option> { + if ids.is_empty() { + return None; + } + + let ids = &ids[0..500.min(ids.len())]; + let mut filters: Vec = vec![]; + + let pks: Vec<&[u8; 32]> = ids + .iter() + .flat_map(|id| id.is_pubkey().map(|pk| pk.bytes())) + .collect(); + if !pks.is_empty() { + let pk_filter = Filter::new().authors(pks).kinds([0]).build(); + filters.push(pk_filter); + } + + let note_ids: Vec<&[u8; 32]> = ids + .iter() + .flat_map(|id| id.is_id().map(|id| id.bytes())) + .collect(); + if !note_ids.is_empty() { + filters.push(Filter::new().ids(note_ids).build()); + } + + Some(filters) +} diff --git a/crates/notedeck_columns/src/user_account.rs b/crates/notedeck/src/user_account.rs similarity index 100% rename from crates/notedeck_columns/src/user_account.rs rename to crates/notedeck/src/user_account.rs diff --git a/crates/notedeck_chrome/Cargo.toml b/crates/notedeck_chrome/Cargo.toml new file mode 100644 index 0000000..a4b0ab5 --- /dev/null +++ b/crates/notedeck_chrome/Cargo.toml @@ -0,0 +1,82 @@ +[package] +name = "notedeck_chrome" +version = "0.2.0" +authors = ["William Casarin ", "kernelkind "] +edition = "2021" +default-run = "notedeck" +#rust-version = "1.60" +license = "GPLv3" +description = "The nostr browser" + +[dependencies] +eframe = { workspace = true } +egui = { workspace = true } +egui_extras = { workspace = true } +enostr = { workspace = true } +nostrdb = { workspace = true } +notedeck = { workspace = true } +notedeck_columns = { workspace = true } +serde_json = { workspace = true } +strum = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-appender = { workspace = true } +tracing-subscriber = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } + +[[bin]] +name = "notedeck" +path = "src/notedeck.rs" + +[[bin]] +name = "ui_preview" +path = "src/preview.rs" + +[features] +default = [] +profiling = ["notedeck_columns/puffin"] + +[target.'cfg(target_os = "android")'.dependencies] +android_logger = "0.11.1" +android-activity = { version = "0.4", features = [ "native-activity" ] } +winit = { version = "0.30.5", features = [ "android-native-activity" ] } +#winit = { git="https://github.com/rust-windowing/winit.git", rev = "2a58b785fed2a3746f7c7eebce95bce67ddfd27c", features = ["android-native-activity"] } + +[package.metadata.bundle] +identifier = "com.damus.notedeck" +icon = ["assets/app_icon.icns"] + +[package.metadata.android] +package = "com.damus.app" +apk_name = "damus" +#assets = "assets" + +[[package.metadata.android.uses_feature]] +name = "android.hardware.vulkan.level" +required = true +version = 1 + +[[package.metadata.android.uses_permission]] +name = "android.permission.WRITE_EXTERNAL_STORAGE" +max_sdk_version = 18 + +[[package.metadata.android.uses_permission]] +name = "android.permission.READ_EXTERNAL_STORAGE" +max_sdk_version = 18 + +[package.metadata.android.signing.release] +path = "damus.keystore" +keystore_password = "damuskeystore" + +[[package.metadata.android.uses_permission]] +name = "android.permission.INTERNET" + +[package.metadata.android.application] +label = "Damus" + +[package.metadata.generate-rpm] +assets = [ + { source = "target/release/notedeck", dest = "/usr/bin/notedeck", mode = "755" }, +] diff --git a/crates/notedeck_columns/src/app_size_handler.rs b/crates/notedeck_chrome/src/app_size.rs similarity index 96% rename from crates/notedeck_columns/src/app_size_handler.rs rename to crates/notedeck_chrome/src/app_size.rs index 3e8d718..d577263 100644 --- a/crates/notedeck_columns/src/app_size_handler.rs +++ b/crates/notedeck_chrome/src/app_size.rs @@ -3,7 +3,7 @@ use std::time::{Duration, Instant}; use egui::Context; use tracing::info; -use crate::storage::{write_file, DataPath, DataPathType, Directory}; +use notedeck::{storage, DataPath, DataPathType, Directory}; pub struct AppSizeHandler { directory: Directory, @@ -71,7 +71,7 @@ fn try_save_size( maybe_saved_size: &mut Option, ) { if let Ok(serialized_rect) = serde_json::to_string(&cur_size) { - if write_file( + if storage::write_file( &interactor.file_path, FILE_NAME.to_owned(), &serialized_rect, diff --git a/crates/notedeck_columns/src/fonts.rs b/crates/notedeck_chrome/src/fonts.rs similarity index 65% rename from crates/notedeck_columns/src/fonts.rs rename to crates/notedeck_chrome/src/fonts.rs index e5be4aa..c0c0a5c 100644 --- a/crates/notedeck_columns/src/fonts.rs +++ b/crates/notedeck_chrome/src/fonts.rs @@ -2,25 +2,7 @@ use egui::{FontData, FontDefinitions, FontTweak}; use std::collections::BTreeMap; use tracing::debug; -pub enum NamedFontFamily { - Medium, - Bold, - Emoji, -} - -impl NamedFontFamily { - pub fn as_str(&mut self) -> &'static str { - match self { - Self::Bold => "bold", - Self::Medium => "medium", - Self::Emoji => "emoji", - } - } - - pub fn as_family(&mut self) -> egui::FontFamily { - egui::FontFamily::Name(self.as_str().into()) - } -} +use notedeck::fonts::NamedFontFamily; // Use gossip's approach to font loading. This includes japanese fonts // for rending stuff from japanese users. @@ -31,26 +13,28 @@ pub fn setup_fonts(ctx: &egui::Context) { font_data.insert( "Onest".to_owned(), FontData::from_static(include_bytes!( - "../assets/fonts/onest/OnestRegular1602-hint.ttf" + "../../../assets/fonts/onest/OnestRegular1602-hint.ttf" )), ); font_data.insert( "OnestMedium".to_owned(), FontData::from_static(include_bytes!( - "../assets/fonts/onest/OnestMedium1602-hint.ttf" + "../../../assets/fonts/onest/OnestMedium1602-hint.ttf" )), ); font_data.insert( "DejaVuSans".to_owned(), - FontData::from_static(include_bytes!("../assets/fonts/DejaVuSansSansEmoji.ttf")), + FontData::from_static(include_bytes!( + "../../../assets/fonts/DejaVuSansSansEmoji.ttf" + )), ); font_data.insert( "OnestBold".to_owned(), FontData::from_static(include_bytes!( - "../assets/fonts/onest/OnestBold1602-hint.ttf" + "../../../assets/fonts/onest/OnestBold1602-hint.ttf" )), ); @@ -76,37 +60,43 @@ pub fn setup_fonts(ctx: &egui::Context) { font_data.insert( "Inconsolata".to_owned(), - FontData::from_static(include_bytes!("../assets/fonts/Inconsolata-Regular.ttf")).tweak( - FontTweak { - scale: 1.22, // This font is smaller than DejaVuSans - y_offset_factor: -0.18, // and too low - y_offset: 0.0, - baseline_offset_factor: 0.0, - }, - ), + FontData::from_static(include_bytes!( + "../../../assets/fonts/Inconsolata-Regular.ttf" + )) + .tweak(FontTweak { + scale: 1.22, // This font is smaller than DejaVuSans + y_offset_factor: -0.18, // and too low + y_offset: 0.0, + baseline_offset_factor: 0.0, + }), ); font_data.insert( "NotoSansCJK".to_owned(), - FontData::from_static(include_bytes!("../assets/fonts/NotoSansCJK-Regular.ttc")), + FontData::from_static(include_bytes!( + "../../../assets/fonts/NotoSansCJK-Regular.ttc" + )), ); font_data.insert( "NotoSansThai".to_owned(), - FontData::from_static(include_bytes!("../assets/fonts/NotoSansThai-Regular.ttf")), + FontData::from_static(include_bytes!( + "../../../assets/fonts/NotoSansThai-Regular.ttf" + )), ); // Some good looking emojis. Use as first priority: font_data.insert( "NotoEmoji".to_owned(), - FontData::from_static(include_bytes!("../assets/fonts/NotoEmoji-Regular.ttf")).tweak( - FontTweak { - scale: 1.1, // make them a touch larger - y_offset_factor: 0.0, - y_offset: 0.0, - baseline_offset_factor: 0.0, - }, - ), + FontData::from_static(include_bytes!( + "../../../assets/fonts/NotoEmoji-Regular.ttf" + )) + .tweak(FontTweak { + scale: 1.1, // make them a touch larger + y_offset_factor: 0.0, + y_offset: 0.0, + baseline_offset_factor: 0.0, + }), ); let base_fonts = vec![ diff --git a/crates/notedeck_chrome/src/lib.rs b/crates/notedeck_chrome/src/lib.rs new file mode 100644 index 0000000..f54e710 --- /dev/null +++ b/crates/notedeck_chrome/src/lib.rs @@ -0,0 +1,4 @@ +pub mod app_size; +pub mod fonts; +pub mod setup; +pub mod theme; diff --git a/crates/notedeck_chrome/src/notedeck.rs b/crates/notedeck_chrome/src/notedeck.rs new file mode 100644 index 0000000..9978974 --- /dev/null +++ b/crates/notedeck_chrome/src/notedeck.rs @@ -0,0 +1,392 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +use notedeck_chrome::{ + app_size::AppSizeHandler, + setup::{generate_native_options, setup_cc}, + theme, +}; + +use notedeck_columns::Damus; + +use notedeck::{ + Accounts, AppContext, Args, DataPath, DataPathType, Directory, FileKeyStorage, ImageCache, + KeyStorageType, NoteCache, ThemeHandler, UnknownIds, +}; + +use enostr::RelayPool; +use nostrdb::{Config, Ndb, Transaction}; +use std::cell::RefCell; +use std::path::Path; +use std::rc::Rc; +use std::{path::PathBuf, str::FromStr}; +use tracing::info; +use tracing_subscriber::EnvFilter; + +/// Our browser app state +struct Notedeck { + ndb: Ndb, + img_cache: ImageCache, + unknown_ids: UnknownIds, + pool: RelayPool, + note_cache: NoteCache, + accounts: Accounts, + path: DataPath, + args: Args, + theme: ThemeHandler, + tabs: Tabs, + app_rect_handler: AppSizeHandler, + egui: egui::Context, +} + +struct Tabs { + app: Option>>, +} + +impl Tabs { + pub fn new(app: Option>>) -> Self { + Self { app } + } +} + +impl eframe::App for Notedeck { + /// Called by the frame work to save state before shutdown. + fn save(&mut self, _storage: &mut dyn eframe::Storage) { + //eframe::set_value(storage, eframe::APP_KEY, self); + } + + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // TODO: render chrome + + // render app + if let Some(app) = &self.tabs.app { + let app = app.clone(); + app.borrow_mut().update(&mut self.app_context()); + } + + self.app_rect_handler.try_save_app_size(ctx); + } +} + +impl Notedeck { + pub fn new>(ctx: &egui::Context, data_path: P, args: &[String]) -> Self { + let parsed_args = Args::parse(args); + let is_mobile = parsed_args + .is_mobile + .unwrap_or(notedeck::ui::is_compiled_as_mobile()); + + // Some people have been running notedeck in debug, let's catch that! + if !cfg!(test) && cfg!(debug_assertions) && !parsed_args.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!("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!("---------------------------------"); + panic!(); + } + + setup_cc(ctx, is_mobile, parsed_args.light); + + let data_path = parsed_args + .datapath + .unwrap_or(data_path.as_ref().to_str().expect("db path ok").to_string()); + let path = DataPath::new(&data_path); + let dbpath_str = parsed_args + .dbpath + .unwrap_or_else(|| path.path(DataPathType::Db).to_str().unwrap().to_string()); + + let _ = std::fs::create_dir_all(&dbpath_str); + + let imgcache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir()); + let _ = std::fs::create_dir_all(imgcache_dir.clone()); + + let mapsize = if cfg!(target_os = "windows") { + // 16 Gib on windows because it actually creates the file + 1024usize * 1024usize * 1024usize * 16usize + } else { + // 1 TiB for everything else since its just virtually mapped + 1024usize * 1024usize * 1024usize * 1024usize + }; + + let theme = ThemeHandler::new(&path); + ctx.options_mut(|o| { + let cur_theme = theme.load(); + info!("Loaded theme {:?} from disk", cur_theme); + o.theme_preference = cur_theme; + }); + ctx.set_visuals_of( + egui::Theme::Dark, + theme::dark_mode(notedeck::ui::is_compiled_as_mobile()), + ); + ctx.set_visuals_of(egui::Theme::Light, theme::light_mode()); + + let config = Config::new().set_ingester_threads(4).set_mapsize(mapsize); + + let keystore = if parsed_args.use_keystore { + let keys_path = path.path(DataPathType::Keys); + let selected_key_path = path.path(DataPathType::SelectedKey); + KeyStorageType::FileSystem(FileKeyStorage::new( + Directory::new(keys_path), + Directory::new(selected_key_path), + )) + } else { + KeyStorageType::None + }; + + let mut accounts = Accounts::new(keystore, parsed_args.relays); + + let num_keys = parsed_args.keys.len(); + + let mut unknown_ids = UnknownIds::default(); + let ndb = Ndb::new(&dbpath_str, &config).expect("ndb"); + + { + let txn = Transaction::new(&ndb).expect("txn"); + for key in parsed_args.keys { + info!("adding account: {}", key.pubkey); + accounts + .add_account(key) + .process_action(&mut unknown_ids, &ndb, &txn); + } + } + + if num_keys != 0 { + accounts.select_account(0); + } + + // AccountManager will setup the pool on first update + let pool = RelayPool::new(); + + let img_cache = ImageCache::new(imgcache_dir); + let note_cache = NoteCache::default(); + let unknown_ids = UnknownIds::default(); + let egui = ctx.clone(); + let tabs = Tabs::new(None); + let parsed_args = Args::parse(args); + let app_rect_handler = AppSizeHandler::new(&path); + + Self { + ndb, + img_cache, + app_rect_handler, + unknown_ids, + pool, + note_cache, + accounts, + path: path.clone(), + args: parsed_args, + theme, + egui, + tabs, + } + } + + pub fn app_context(&mut self) -> AppContext<'_> { + AppContext { + ndb: &self.ndb, + img_cache: &mut self.img_cache, + unknown_ids: &mut self.unknown_ids, + pool: &mut self.pool, + note_cache: &mut self.note_cache, + accounts: &mut self.accounts, + path: &self.path, + args: &self.args, + theme: &mut self.theme, + egui: &self.egui, + } + } + + pub fn add_app(&mut self, app: T) { + self.tabs.app = Some(Rc::new(RefCell::new(app))); + } +} + +// Entry point for wasm +//#[cfg(target_arch = "wasm32")] +//use wasm_bindgen::prelude::*; + +fn setup_logging(path: &DataPath) { + #[allow(unused_variables)] // need guard to live for lifetime of program + let (maybe_non_blocking, maybe_guard) = { + let log_path = path.path(DataPathType::Log); + // Setup logging to file + + use tracing_appender::{ + non_blocking, + rolling::{RollingFileAppender, Rotation}, + }; + + let file_appender = RollingFileAppender::new( + Rotation::DAILY, + log_path, + format!("notedeck-{}.log", env!("CARGO_PKG_VERSION")), + ); + + let (non_blocking, _guard) = non_blocking(file_appender); + + (Some(non_blocking), Some(_guard)) + }; + + // Log to stdout (if you run with `RUST_LOG=debug`). + if let Some(non_blocking_writer) = maybe_non_blocking { + use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt}; + + let console_layer = fmt::layer().with_target(true).with_writer(std::io::stdout); + + // Create the file layer (writes to the file) + let file_layer = fmt::layer() + .with_ansi(false) + .with_writer(non_blocking_writer); + + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("notedeck=info")); + + // Set up the subscriber to combine both layers + tracing_subscriber::registry() + .with(console_layer) + .with(file_layer) + .with(env_filter) + .init(); + } else { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .init(); + } +} + +// Desktop +#[cfg(not(target_arch = "wasm32"))] +#[tokio::main] +async fn main() { + let base_path = DataPath::default_base().unwrap_or(PathBuf::from_str(".").unwrap()); + let path = DataPath::new(&base_path); + + setup_logging(&path); + + let _res = eframe::run_native( + "Damus Notedeck", + generate_native_options(path), + Box::new(|cc| { + let args: Vec = std::env::args().collect(); + let mut notedeck = Notedeck::new(&cc.egui_ctx, base_path, &args); + + let damus = Damus::new(&mut notedeck.app_context(), &args); + notedeck.add_app(damus); + + Ok(Box::new(notedeck)) + }), + ); +} + +/* + * TODO: nostrdb not supported on web + * +#[cfg(target_arch = "wasm32")] +pub fn main() { + // Make sure panics are logged using `console.error`. + console_error_panic_hook::set_once(); + + // Redirect tracing to console.log and friends: + tracing_wasm::set_as_global_default(); + + wasm_bindgen_futures::spawn_local(async { + let web_options = eframe::WebOptions::default(); + eframe::start_web( + "the_canvas_id", // hardcode it + web_options, + Box::new(|cc| Box::new(Damus::new(cc, "."))), + ) + .await + .expect("failed to start eframe"); + }); +} +*/ + +#[cfg(test)] +mod tests { + use super::{Damus, Notedeck}; + use std::path::{Path, PathBuf}; + + fn create_tmp_dir() -> PathBuf { + tempfile::TempDir::new() + .expect("tmp path") + .path() + .to_path_buf() + } + + fn rmrf(path: impl AsRef) { + let _ = std::fs::remove_dir_all(path); + } + + /// Ensure dbpath actually sets the dbpath correctly. + #[tokio::test] + async fn test_dbpath() { + let datapath = create_tmp_dir(); + let dbpath = create_tmp_dir(); + let args: Vec = vec![ + "--datapath", + &datapath.to_str().unwrap(), + "--dbpath", + &dbpath.to_str().unwrap(), + ] + .iter() + .map(|s| s.to_string()) + .collect(); + + let ctx = egui::Context::default(); + let _app = Notedeck::new(&ctx, &datapath, &args); + + assert!(Path::new(&dbpath.join("data.mdb")).exists()); + assert!(Path::new(&dbpath.join("lock.mdb")).exists()); + assert!(!Path::new(&datapath.join("db")).exists()); + + rmrf(datapath); + rmrf(dbpath); + } + + #[tokio::test] + async fn test_column_args() { + let tmpdir = create_tmp_dir(); + let npub = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s"; + let args: Vec = vec![ + "--no-keystore", + "--pub", + npub, + "-c", + "notifications", + "-c", + "contacts", + ] + .iter() + .map(|s| s.to_string()) + .collect(); + + let ctx = egui::Context::default(); + let mut notedeck = Notedeck::new(&ctx, &tmpdir, &args); + let mut app_ctx = notedeck.app_context(); + let app = Damus::new(&mut app_ctx, &args); + + assert_eq!(app.columns(app_ctx.accounts).columns().len(), 2); + + let tl1 = app + .columns(app_ctx.accounts) + .column(0) + .router() + .top() + .timeline_id(); + + let tl2 = app + .columns(app_ctx.accounts) + .column(1) + .router() + .top() + .timeline_id(); + + assert_eq!(tl1.is_some(), true); + assert_eq!(tl2.is_some(), true); + + let timelines = app.columns(app_ctx.accounts).timelines(); + assert!(timelines[0].kind.is_notifications()); + assert!(timelines[1].kind.is_contacts()); + + rmrf(tmpdir); + } +} diff --git a/crates/notedeck/src/preview.rs b/crates/notedeck_chrome/src/preview.rs similarity index 83% rename from crates/notedeck/src/preview.rs rename to crates/notedeck_chrome/src/preview.rs index b17fca5..a9fa529 100644 --- a/crates/notedeck/src/preview.rs +++ b/crates/notedeck_chrome/src/preview.rs @@ -1,13 +1,12 @@ +use notedeck::DataPath; +use notedeck_chrome::setup::{ + generate_mobile_emulator_native_options, generate_native_options, setup_cc, +}; use notedeck_columns::ui::configure_deck::ConfigureDeckView; use notedeck_columns::ui::edit_deck::EditDeckView; use notedeck_columns::ui::{ - account_login_view::AccountLoginView, accounts::AccountsView, add_column::AddColumnView, - DesktopSidePanel, PostView, Preview, PreviewApp, PreviewConfig, ProfilePic, ProfilePreview, - RelayView, -}; -use notedeck_columns::{ - app_creation::{generate_mobile_emulator_native_options, generate_native_options, setup_cc}, - storage::DataPath, + account_login_view::AccountLoginView, PostView, Preview, PreviewApp, PreviewConfig, ProfilePic, + ProfilePreview, RelayView, }; use std::env; @@ -93,7 +92,7 @@ async fn main() { "light mode previews: {}", if light_mode { "enabled" } else { "disabled" } ); - let is_mobile = is_mobile.unwrap_or(notedeck_columns::ui::is_compiled_as_mobile()); + let is_mobile = is_mobile.unwrap_or(notedeck::ui::is_compiled_as_mobile()); let runner = PreviewRunner::new(is_mobile, light_mode); previews!( @@ -104,10 +103,7 @@ async fn main() { AccountLoginView, ProfilePreview, ProfilePic, - AccountsView, - DesktopSidePanel, PostView, - AddColumnView, ConfigureDeckView, EditDeckView, ); diff --git a/crates/notedeck_chrome/src/setup.rs b/crates/notedeck_chrome/src/setup.rs new file mode 100644 index 0000000..f7c9236 --- /dev/null +++ b/crates/notedeck_chrome/src/setup.rs @@ -0,0 +1,79 @@ +use crate::{app_size::AppSizeHandler, fonts, theme}; + +use eframe::NativeOptions; +use notedeck::DataPath; + +pub fn setup_cc(ctx: &egui::Context, is_mobile: bool, light: bool) { + fonts::setup_fonts(ctx); + + //ctx.set_pixels_per_point(ctx.pixels_per_point() + UI_SCALE_FACTOR); + //ctx.set_pixels_per_point(1.0); + // + // + //ctx.tessellation_options_mut(|to| to.feathering = false); + + egui_extras::install_image_loaders(ctx); + + if light { + ctx.set_visuals(theme::light_mode()) + } else { + ctx.set_visuals(theme::dark_mode(is_mobile)); + } + + ctx.all_styles_mut(|style| theme::add_custom_style(is_mobile, style)); +} + +//pub const UI_SCALE_FACTOR: f32 = 0.2; + +pub fn generate_native_options(paths: DataPath) -> NativeOptions { + let window_builder = Box::new(move |builder: egui::ViewportBuilder| { + let builder = builder + .with_fullsize_content_view(true) + .with_titlebar_shown(false) + .with_title_shown(false) + .with_icon(std::sync::Arc::new( + eframe::icon_data::from_png_bytes(app_icon()).expect("icon"), + )); + + if let Some(window_size) = AppSizeHandler::new(&paths).get_app_size() { + builder.with_inner_size(window_size) + } else { + builder + } + }); + + eframe::NativeOptions { + window_builder: Some(window_builder), + viewport: egui::ViewportBuilder::default().with_icon(std::sync::Arc::new( + eframe::icon_data::from_png_bytes(app_icon()).expect("icon"), + )), + ..Default::default() + } +} + +fn generate_native_options_with_builder_modifiers( + apply_builder_modifiers: fn(egui::ViewportBuilder) -> egui::ViewportBuilder, +) -> NativeOptions { + let window_builder = + Box::new(move |builder: egui::ViewportBuilder| apply_builder_modifiers(builder)); + + eframe::NativeOptions { + window_builder: Some(window_builder), + ..Default::default() + } +} + +pub fn app_icon() -> &'static [u8; 271986] { + std::include_bytes!("../../../assets/damus-app-icon.png") +} + +pub fn generate_mobile_emulator_native_options() -> eframe::NativeOptions { + generate_native_options_with_builder_modifiers(|builder| { + builder + .with_fullsize_content_view(true) + .with_titlebar_shown(false) + .with_title_shown(false) + .with_inner_size([405.0, 915.0]) + .with_icon(eframe::icon_data::from_png_bytes(app_icon()).expect("icon")) + }) +} diff --git a/crates/notedeck_chrome/src/theme.rs b/crates/notedeck_chrome/src/theme.rs new file mode 100644 index 0000000..c20da79 --- /dev/null +++ b/crates/notedeck_chrome/src/theme.rs @@ -0,0 +1,132 @@ +use egui::{style::Interaction, Color32, FontId, Style, Visuals}; +use notedeck::{ColorTheme, NotedeckTextStyle}; +use strum::IntoEnumIterator; + +pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5); +const PURPLE_ALT: Color32 = Color32::from_rgb(0x82, 0x56, 0xDD); +//pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52); +pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A); +const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00); +const RED_700: Color32 = Color32::from_rgb(0xC7, 0x37, 0x5A); +const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A); + +// BACKGROUNDS +const SEMI_DARKER_BG: Color32 = Color32::from_rgb(0x39, 0x39, 0x39); +const DARKER_BG: Color32 = Color32::from_rgb(0x1F, 0x1F, 0x1F); +const DARK_BG: Color32 = Color32::from_rgb(0x2C, 0x2C, 0x2C); +const DARK_ISH_BG: Color32 = Color32::from_rgb(0x25, 0x25, 0x25); +const SEMI_DARK_BG: Color32 = Color32::from_rgb(0x44, 0x44, 0x44); + +const LIGHTER_GRAY: Color32 = Color32::from_rgb(0xf8, 0xf8, 0xf8); +const LIGHT_GRAY: Color32 = Color32::from_rgb(0xc8, 0xc8, 0xc8); // 78% +const DARKER_GRAY: Color32 = Color32::from_rgb(0xa5, 0xa5, 0xa5); // 65% +const EVEN_DARKER_GRAY: Color32 = Color32::from_rgb(0x89, 0x89, 0x89); // 54% + +pub fn desktop_dark_color_theme() -> ColorTheme { + ColorTheme { + // VISUALS + panel_fill: DARKER_BG, + extreme_bg_color: DARK_ISH_BG, + text_color: Color32::WHITE, + err_fg_color: RED_700, + warn_fg_color: ORANGE_700, + hyperlink_color: PURPLE, + selection_color: PURPLE_ALT, + + // WINDOW + window_fill: DARK_ISH_BG, + window_stroke_color: DARK_BG, + + // NONINTERACTIVE WIDGET + noninteractive_bg_fill: DARK_ISH_BG, + noninteractive_weak_bg_fill: DARK_BG, + noninteractive_bg_stroke_color: SEMI_DARKER_BG, + noninteractive_fg_stroke_color: GRAY_SECONDARY, + + // INACTIVE WIDGET + inactive_bg_stroke_color: SEMI_DARKER_BG, + inactive_bg_fill: Color32::from_rgb(0x25, 0x25, 0x25), + inactive_weak_bg_fill: SEMI_DARK_BG, + } +} + +pub fn mobile_dark_color_theme() -> ColorTheme { + ColorTheme { + panel_fill: Color32::BLACK, + noninteractive_weak_bg_fill: Color32::from_rgb(0x1F, 0x1F, 0x1F), + ..desktop_dark_color_theme() + } +} + +pub fn light_color_theme() -> ColorTheme { + ColorTheme { + // VISUALS + panel_fill: Color32::WHITE, + extreme_bg_color: LIGHTER_GRAY, + text_color: BLACK, + err_fg_color: RED_700, + warn_fg_color: ORANGE_700, + hyperlink_color: PURPLE, + selection_color: PURPLE_ALT, + + // WINDOW + window_fill: Color32::WHITE, + window_stroke_color: DARKER_GRAY, + + // NONINTERACTIVE WIDGET + noninteractive_bg_fill: Color32::WHITE, + noninteractive_weak_bg_fill: LIGHTER_GRAY, + noninteractive_bg_stroke_color: LIGHT_GRAY, + noninteractive_fg_stroke_color: GRAY_SECONDARY, + + // INACTIVE WIDGET + inactive_bg_stroke_color: EVEN_DARKER_GRAY, + inactive_bg_fill: LIGHT_GRAY, + inactive_weak_bg_fill: EVEN_DARKER_GRAY, + } +} + +pub fn light_mode() -> Visuals { + notedeck::theme::create_themed_visuals(light_color_theme(), Visuals::light()) +} + +pub fn dark_mode(mobile: bool) -> Visuals { + notedeck::theme::create_themed_visuals( + if mobile { + mobile_dark_color_theme() + } else { + desktop_dark_color_theme() + }, + Visuals::dark(), + ) +} + +/// Create custom text sizes for any FontSizes +pub fn add_custom_style(is_mobile: bool, style: &mut Style) { + let font_size = if is_mobile { + notedeck::fonts::mobile_font_size + } else { + notedeck::fonts::desktop_font_size + }; + + style.text_styles = NotedeckTextStyle::iter() + .map(|text_style| { + ( + text_style.text_style(), + FontId::new(font_size(&text_style), text_style.font_family()), + ) + }) + .collect(); + + style.interaction = Interaction { + tooltip_delay: 0.1, + show_tooltips_only_when_still: false, + ..Interaction::default() + }; + + #[cfg(debug_assertions)] + { + style.debug.show_interactive_widgets = true; + style.debug.debug_on_hover_with_all_modifiers = true; + } +} diff --git a/crates/notedeck_columns/Cargo.toml b/crates/notedeck_columns/Cargo.toml index 6b8d0a2..a2a082d 100644 --- a/crates/notedeck_columns/Cargo.toml +++ b/crates/notedeck_columns/Cargo.toml @@ -11,10 +11,11 @@ description = "A tweetdeck-style notedeck app" crate-type = ["lib", "cdylib"] [dependencies] -base32 = { workspace = true } +notedeck = { workspace = true } bitflags = { workspace = true } dirs = { workspace = true } eframe = { workspace = true } +thiserror = { workspace = true } egui = { workspace = true } egui_extras = { workspace = true } egui_nav = { workspace = true } @@ -47,7 +48,7 @@ urlencoding = { workspace = true } uuid = { workspace = true } [dev-dependencies] -tempfile = "3.13.0" +tempfile = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] security-framework = "2.11.0" diff --git a/crates/notedeck_columns/src/accounts/mod.rs b/crates/notedeck_columns/src/accounts/mod.rs index 3b21cf2..1c93837 100644 --- a/crates/notedeck_columns/src/accounts/mod.rs +++ b/crates/notedeck_columns/src/accounts/mod.rs @@ -1,245 +1,23 @@ -use std::cmp::Ordering; -use std::collections::{BTreeMap, BTreeSet}; -use std::sync::Arc; +use enostr::FullKeypair; +use nostrdb::Ndb; -use url::Url; -use uuid::Uuid; - -use enostr::{ClientMessage, FilledKeypair, FullKeypair, Keypair, RelayPool}; -use nostrdb::{Filter, Ndb, Note, NoteKey, Subscription, Transaction}; +use notedeck::{Accounts, AccountsAction, AddAccountAction, ImageCache, SingleUnkIdAction}; use crate::app::get_active_columns_mut; use crate::decks::DecksCache; use crate::{ - imgcache::ImageCache, login_manager::AcquireKeyState, - muted::Muted, route::Route, - storage::{KeyStorageResponse, KeyStorageType}, ui::{ account_login_view::{AccountLoginResponse, AccountLoginView}, accounts::{AccountsView, AccountsViewResponse}, }, - unknowns::SingleUnkIdAction, - unknowns::UnknownIds, - user_account::UserAccount, }; -use tracing::{debug, error, info}; +use tracing::info; mod route; -pub use route::{AccountsAction, AccountsRoute, AccountsRouteResponse}; - -pub struct AccountRelayData { - filter: Filter, - subid: String, - sub: Option, - local: BTreeSet, // used locally but not advertised - advertised: BTreeSet, // advertised via NIP-65 -} - -impl AccountRelayData { - pub fn new(ndb: &Ndb, pool: &mut RelayPool, pubkey: &[u8; 32]) -> Self { - // Construct a filter for the user's NIP-65 relay list - let filter = Filter::new() - .authors([pubkey]) - .kinds([10002]) - .limit(1) - .build(); - - // Local ndb subscription - let ndbsub = ndb - .subscribe(&[filter.clone()]) - .expect("ndb relay list subscription"); - - // Query the ndb immediately to see if the user list is already there - let txn = Transaction::new(ndb).expect("transaction"); - let lim = filter.limit().unwrap_or(crate::filter::default_limit()) as i32; - let nks = ndb - .query(&txn, &[filter.clone()], lim) - .expect("query user relays results") - .iter() - .map(|qr| qr.note_key) - .collect::>(); - let relays = Self::harvest_nip65_relays(ndb, &txn, &nks); - debug!( - "pubkey {}: initial relays {:?}", - hex::encode(pubkey), - relays - ); - - // Id for future remote relay subscriptions - let subid = Uuid::new_v4().to_string(); - - // Add remote subscription to existing relays - pool.subscribe(subid.clone(), vec![filter.clone()]); - - AccountRelayData { - filter, - subid, - sub: Some(ndbsub), - local: BTreeSet::new(), - advertised: relays.into_iter().collect(), - } - } - - // standardize the format (ie, trailing slashes) to avoid dups - pub fn canonicalize_url(url: &str) -> String { - match Url::parse(url) { - Ok(parsed_url) => parsed_url.to_string(), - Err(_) => url.to_owned(), // If parsing fails, return the original URL. - } - } - - fn harvest_nip65_relays(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Vec { - let mut relays = Vec::new(); - for nk in nks.iter() { - if let Ok(note) = ndb.get_note_by_key(txn, *nk) { - for tag in note.tags() { - match tag.get(0).and_then(|t| t.variant().str()) { - Some("r") => { - if let Some(url) = tag.get(1).and_then(|f| f.variant().str()) { - relays.push(Self::canonicalize_url(url)); - } - } - Some("alt") => { - // ignore for now - } - Some(x) => { - error!("harvest_nip65_relays: unexpected tag type: {}", x); - } - None => { - error!("harvest_nip65_relays: invalid tag"); - } - } - } - } - } - relays - } -} - -pub struct AccountMutedData { - filter: Filter, - subid: String, - sub: Option, - muted: Arc, -} - -impl AccountMutedData { - pub fn new(ndb: &Ndb, pool: &mut RelayPool, pubkey: &[u8; 32]) -> Self { - // Construct a filter for the user's NIP-51 muted list - let filter = Filter::new() - .authors([pubkey]) - .kinds([10000]) - .limit(1) - .build(); - - // Local ndb subscription - let ndbsub = ndb - .subscribe(&[filter.clone()]) - .expect("ndb muted subscription"); - - // Query the ndb immediately to see if the user's muted list is already there - let txn = Transaction::new(ndb).expect("transaction"); - let lim = filter.limit().unwrap_or(crate::filter::default_limit()) as i32; - let nks = ndb - .query(&txn, &[filter.clone()], lim) - .expect("query user muted results") - .iter() - .map(|qr| qr.note_key) - .collect::>(); - let muted = Self::harvest_nip51_muted(ndb, &txn, &nks); - debug!("pubkey {}: initial muted {:?}", hex::encode(pubkey), muted); - - // Id for future remote relay subscriptions - let subid = Uuid::new_v4().to_string(); - - // Add remote subscription to existing relays - pool.subscribe(subid.clone(), vec![filter.clone()]); - - AccountMutedData { - filter, - subid, - sub: Some(ndbsub), - muted: Arc::new(muted), - } - } - - fn harvest_nip51_muted(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Muted { - let mut muted = Muted::default(); - for nk in nks.iter() { - if let Ok(note) = ndb.get_note_by_key(txn, *nk) { - for tag in note.tags() { - match tag.get(0).and_then(|t| t.variant().str()) { - Some("p") => { - if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) { - muted.pubkeys.insert(*id); - } - } - Some("t") => { - if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) { - muted.hashtags.insert(str.to_string()); - } - } - Some("word") => { - if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) { - muted.words.insert(str.to_string()); - } - } - Some("e") => { - if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) { - muted.threads.insert(*id); - } - } - Some("alt") => { - // maybe we can ignore these? - } - Some(x) => error!("query_nip51_muted: unexpected tag: {}", x), - None => error!( - "query_nip51_muted: bad tag value: {:?}", - tag.get_unchecked(0).variant() - ), - } - } - } - } - muted - } -} - -pub struct AccountData { - relay: AccountRelayData, - muted: AccountMutedData, -} - -/// The interface for managing the user's accounts. -/// Represents all user-facing operations related to account management. -pub struct Accounts { - currently_selected_account: Option, - accounts: Vec, - key_store: KeyStorageType, - account_data: BTreeMap<[u8; 32], AccountData>, - forced_relays: BTreeSet, - bootstrap_relays: BTreeSet, - needs_relay_config: bool, -} - -#[must_use = "You must call process_login_action on this to handle unknown ids"] -pub struct RenderAccountAction { - pub accounts_action: Option, - pub unk_id_action: SingleUnkIdAction, -} - -impl RenderAccountAction { - // Simple wrapper around processing the unknown action to expose too - // much internal logic. This allows us to have a must_use on our - // LoginAction type, otherwise the SingleUnkIdAction's must_use will - // be lost when returned in the login action - pub fn process_action(&mut self, ids: &mut UnknownIds, ndb: &Ndb, txn: &Transaction) { - self.unk_id_action.process_action(ids, ndb, txn); - } -} +pub use route::{AccountsRoute, AccountsRouteResponse}; /// Render account management views from a route #[allow(clippy::too_many_arguments)] @@ -252,7 +30,7 @@ pub fn render_accounts_route( decks: &mut DecksCache, login_state: &mut AcquireKeyState, route: AccountsRoute, -) -> RenderAccountAction { +) -> AddAccountAction { let resp = match route { AccountsRoute::Accounts => AccountsView::new(ndb, accounts, img_cache) .ui(ui) @@ -269,7 +47,7 @@ pub fn render_accounts_route( match resp { AccountsRouteResponse::Accounts(response) => { let action = process_accounts_view_response(accounts, decks, col, response); - RenderAccountAction { + AddAccountAction { accounts_action: action, unk_id_action: SingleUnkIdAction::no_action(), } @@ -285,7 +63,7 @@ pub fn render_accounts_route( } } } else { - RenderAccountAction { + AddAccountAction { accounts_action: None, unk_id_action: SingleUnkIdAction::no_action(), } @@ -321,343 +99,11 @@ pub fn process_accounts_view_response( selection } -impl Accounts { - pub fn new(key_store: KeyStorageType, forced_relays: Vec) -> Self { - let accounts = if let KeyStorageResponse::ReceivedResult(res) = key_store.get_keys() { - res.unwrap_or_default() - } else { - Vec::new() - }; - - let currently_selected_account = get_selected_index(&accounts, &key_store); - let account_data = BTreeMap::new(); - let forced_relays: BTreeSet = forced_relays - .into_iter() - .map(|u| AccountRelayData::canonicalize_url(&u)) - .collect(); - let bootstrap_relays = [ - "wss://relay.damus.io", - // "wss://pyramid.fiatjaf.com", // Uncomment if needed - "wss://nos.lol", - "wss://nostr.wine", - "wss://purplepag.es", - ] - .iter() - .map(|&url| url.to_string()) - .map(|u| AccountRelayData::canonicalize_url(&u)) - .collect(); - - Accounts { - currently_selected_account, - accounts, - key_store, - account_data, - forced_relays, - bootstrap_relays, - needs_relay_config: true, - } - } - - pub fn get_accounts(&self) -> &Vec { - &self.accounts - } - - pub fn get_account(&self, ind: usize) -> Option<&UserAccount> { - self.accounts.get(ind) - } - - pub fn find_account(&self, pk: &[u8; 32]) -> Option<&UserAccount> { - self.accounts.iter().find(|acc| acc.pubkey.bytes() == pk) - } - - pub fn remove_account(&mut self, index: usize) { - if let Some(account) = self.accounts.get(index) { - let _ = self.key_store.remove_key(account); - self.accounts.remove(index); - - if let Some(selected_index) = self.currently_selected_account { - match selected_index.cmp(&index) { - Ordering::Greater => { - self.select_account(selected_index - 1); - } - Ordering::Equal => { - if self.accounts.is_empty() { - // If no accounts remain, clear the selection - self.clear_selected_account(); - } else if index >= self.accounts.len() { - // If the removed account was the last one, select the new last account - self.select_account(self.accounts.len() - 1); - } else { - // Otherwise, select the account at the same position - self.select_account(index); - } - } - Ordering::Less => {} - } - } - } - } - - fn contains_account(&self, pubkey: &[u8; 32]) -> Option { - for (index, account) in self.accounts.iter().enumerate() { - let has_pubkey = account.pubkey.bytes() == pubkey; - let has_nsec = account.secret_key.is_some(); - if has_pubkey { - return Some(ContainsAccount { has_nsec, index }); - } - } - - None - } - - #[must_use = "UnknownIdAction's must be handled. Use .process_unknown_id_action()"] - pub fn add_account(&mut self, account: Keypair) -> RenderAccountAction { - let pubkey = account.pubkey; - let switch_to_index = if let Some(contains_acc) = self.contains_account(pubkey.bytes()) { - if account.secret_key.is_some() && !contains_acc.has_nsec { - info!( - "user provided nsec, but we already have npub {}. Upgrading to nsec", - pubkey - ); - let _ = self.key_store.add_key(&account); - - self.accounts[contains_acc.index] = account; - } else { - info!("already have account, not adding {}", pubkey); - } - contains_acc.index - } else { - info!("adding new account {}", pubkey); - let _ = self.key_store.add_key(&account); - self.accounts.push(account); - self.accounts.len() - 1 - }; - - RenderAccountAction { - accounts_action: Some(AccountsAction::Switch(switch_to_index)), - unk_id_action: SingleUnkIdAction::pubkey(pubkey), - } - } - - pub fn num_accounts(&self) -> usize { - self.accounts.len() - } - - pub fn get_selected_account_index(&self) -> Option { - self.currently_selected_account - } - - pub fn selected_or_first_nsec(&self) -> Option> { - self.get_selected_account() - .and_then(|kp| kp.to_full()) - .or_else(|| self.accounts.iter().find_map(|a| a.to_full())) - } - - pub fn get_selected_account(&self) -> Option<&UserAccount> { - if let Some(account_index) = self.currently_selected_account { - if let Some(account) = self.get_account(account_index) { - Some(account) - } else { - None - } - } else { - None - } - } - - pub fn select_account(&mut self, index: usize) { - if let Some(account) = self.accounts.get(index) { - self.currently_selected_account = Some(index); - self.key_store.select_key(Some(account.pubkey)); - } - } - - pub fn clear_selected_account(&mut self) { - self.currently_selected_account = None; - self.key_store.select_key(None); - } - - pub fn mutefun(&self) -> Box bool> { - if let Some(index) = self.currently_selected_account { - if let Some(account) = self.accounts.get(index) { - let pubkey = account.pubkey.bytes(); - if let Some(account_data) = self.account_data.get(pubkey) { - let muted = Arc::clone(&account_data.muted.muted); - return Box::new(move |note: &Note| muted.is_muted(note)); - } - } - } - Box::new(|_: &Note| false) - } - - pub fn send_initial_filters(&mut self, pool: &mut RelayPool, relay_url: &str) { - for data in self.account_data.values() { - pool.send_to( - &ClientMessage::req(data.relay.subid.clone(), vec![data.relay.filter.clone()]), - relay_url, - ); - pool.send_to( - &ClientMessage::req(data.muted.subid.clone(), vec![data.muted.filter.clone()]), - relay_url, - ); - } - } - - // Returns added and removed accounts - fn delta_accounts(&self) -> (Vec<[u8; 32]>, Vec<[u8; 32]>) { - let mut added = Vec::new(); - for pubkey in self.accounts.iter().map(|a| a.pubkey.bytes()) { - if !self.account_data.contains_key(pubkey) { - added.push(*pubkey); - } - } - let mut removed = Vec::new(); - for pubkey in self.account_data.keys() { - if self.contains_account(pubkey).is_none() { - removed.push(*pubkey); - } - } - (added, removed) - } - - fn handle_added_account(&mut self, ndb: &Ndb, pool: &mut RelayPool, pubkey: &[u8; 32]) { - debug!("handle_added_account {}", hex::encode(pubkey)); - - // Create the user account data - let new_account_data = AccountData { - relay: AccountRelayData::new(ndb, pool, pubkey), - muted: AccountMutedData::new(ndb, pool, pubkey), - }; - self.account_data.insert(*pubkey, new_account_data); - } - - fn handle_removed_account(&mut self, pubkey: &[u8; 32]) { - debug!("handle_removed_account {}", hex::encode(pubkey)); - // FIXME - we need to unsubscribe here - self.account_data.remove(pubkey); - } - - fn poll_for_updates(&mut self, ndb: &Ndb) -> bool { - let mut changed = false; - for (pubkey, data) in &mut self.account_data { - if let Some(sub) = data.relay.sub { - let nks = ndb.poll_for_notes(sub, 1); - if !nks.is_empty() { - let txn = Transaction::new(ndb).expect("txn"); - let relays = AccountRelayData::harvest_nip65_relays(ndb, &txn, &nks); - debug!( - "pubkey {}: updated relays {:?}", - hex::encode(pubkey), - relays - ); - data.relay.advertised = relays.into_iter().collect(); - changed = true; - } - } - if let Some(sub) = data.muted.sub { - let nks = ndb.poll_for_notes(sub, 1); - if !nks.is_empty() { - let txn = Transaction::new(ndb).expect("txn"); - let muted = AccountMutedData::harvest_nip51_muted(ndb, &txn, &nks); - debug!("pubkey {}: updated muted {:?}", hex::encode(pubkey), muted); - data.muted.muted = Arc::new(muted); - changed = true; - } - } - } - changed - } - - fn update_relay_configuration( - &mut self, - pool: &mut RelayPool, - wakeup: impl Fn() + Send + Sync + Clone + 'static, - ) { - // If forced relays are set use them only - let mut desired_relays = self.forced_relays.clone(); - - // Compose the desired relay lists from the accounts - if desired_relays.is_empty() { - for data in self.account_data.values() { - desired_relays.extend(data.relay.local.iter().cloned()); - desired_relays.extend(data.relay.advertised.iter().cloned()); - } - } - - // If no relays are specified at this point use the bootstrap list - if desired_relays.is_empty() { - desired_relays = self.bootstrap_relays.clone(); - } - - debug!("current relays: {:?}", pool.urls()); - debug!("desired relays: {:?}", desired_relays); - - let add: BTreeSet = desired_relays.difference(&pool.urls()).cloned().collect(); - let sub: BTreeSet = pool.urls().difference(&desired_relays).cloned().collect(); - if !add.is_empty() { - debug!("configuring added relays: {:?}", add); - let _ = pool.add_urls(add, wakeup); - } - if !sub.is_empty() { - debug!("removing unwanted relays: {:?}", sub); - pool.remove_urls(&sub); - } - - debug!("current relays: {:?}", pool.urls()); - } - - pub fn update(&mut self, ndb: &Ndb, pool: &mut RelayPool, ctx: &egui::Context) { - // IMPORTANT - This function is called in the UI update loop, - // make sure it is fast when idle - - // On the initial update the relays need config even if nothing changes below - let mut relays_changed = self.needs_relay_config; - - let ctx2 = ctx.clone(); - let wakeup = move || { - ctx2.request_repaint(); - }; - - // Were any accounts added or removed? - let (added, removed) = self.delta_accounts(); - for pk in added { - self.handle_added_account(ndb, pool, &pk); - relays_changed = true; - } - for pk in removed { - self.handle_removed_account(&pk); - relays_changed = true; - } - - // Did any accounts receive updates (ie NIP-65 relay lists) - relays_changed = self.poll_for_updates(ndb) || relays_changed; - - // If needed, update the relay configuration - if relays_changed { - self.update_relay_configuration(pool, wakeup); - self.needs_relay_config = false; - } - } -} - -fn get_selected_index(accounts: &[UserAccount], keystore: &KeyStorageType) -> Option { - match keystore.get_selected_key() { - KeyStorageResponse::ReceivedResult(Ok(Some(pubkey))) => { - return accounts.iter().position(|account| account.pubkey == pubkey); - } - - KeyStorageResponse::ReceivedResult(Err(e)) => error!("Error getting selected key: {}", e), - KeyStorageResponse::Waiting | KeyStorageResponse::ReceivedResult(Ok(None)) => {} - }; - - None -} - pub fn process_login_view_response( manager: &mut Accounts, decks: &mut DecksCache, response: AccountLoginResponse, -) -> RenderAccountAction { +) -> AddAccountAction { let (r, pubkey) = match response { AccountLoginResponse::CreateNew => { let kp = FullKeypair::generate().to_keypair(); @@ -674,9 +120,3 @@ pub fn process_login_view_response( r } - -#[derive(Default)] -struct ContainsAccount { - pub has_nsec: bool, - pub index: usize, -} diff --git a/crates/notedeck_columns/src/accounts/route.rs b/crates/notedeck_columns/src/accounts/route.rs index b446d46..69ce127 100644 --- a/crates/notedeck_columns/src/accounts/route.rs +++ b/crates/notedeck_columns/src/accounts/route.rs @@ -11,9 +11,3 @@ pub enum AccountsRoute { Accounts, AddAccount, } - -#[derive(Debug)] -pub enum AccountsAction { - Switch(usize), - Remove(usize), -} diff --git a/crates/notedeck_columns/src/actionbar.rs b/crates/notedeck_columns/src/actionbar.rs index c5979ab..e8d42f0 100644 --- a/crates/notedeck_columns/src/actionbar.rs +++ b/crates/notedeck_columns/src/actionbar.rs @@ -1,15 +1,14 @@ use crate::{ column::Columns, - muted::MuteFun, - note::NoteRef, - notecache::NoteCache, notes_holder::{NotesHolder, NotesHolderStorage}, profile::Profile, route::{Route, Router}, thread::Thread, }; + use enostr::{NoteId, Pubkey, RelayPool}; use nostrdb::{Ndb, Transaction}; +use notedeck::{note::root_note_id_from_selected_id, MuteFun, NoteCache, NoteRef}; #[derive(Debug, Eq, PartialEq, Copy, Clone)] pub enum NoteAction { @@ -46,7 +45,7 @@ fn open_thread( ) -> Option { router.route_to(Route::thread(NoteId::new(selected_note.to_owned()))); - let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, txn, selected_note); + let root_id = root_note_id_from_selected_id(ndb, note_cache, txn, selected_note); Thread::open(ndb, note_cache, txn, pool, threads, root_id, is_muted) } diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs index 0fde4ce..656da3d 100644 --- a/crates/notedeck_columns/src/app.rs +++ b/crates/notedeck_columns/src/app.rs @@ -1,38 +1,31 @@ use crate::{ - accounts::Accounts, - app_creation::setup_cc, - app_size_handler::AppSizeHandler, - app_style::{dark_mode, light_mode}, - args::Args, + args::ColumnsArgs, column::Columns, decks::{Decks, DecksCache, FALLBACK_PUBKEY}, draft::Drafts, - filter::FilterState, - frame_history::FrameHistory, - imgcache::ImageCache, nav, - notecache::NoteCache, notes_holder::NotesHolderStorage, profile::Profile, - storage::{self, DataPath, DataPathType, Directory, FileKeyStorage, KeyStorageType}, + storage, subscriptions::{SubKind, Subscriptions}, support::Support, - theme_handler::ThemeHandler, thread::Thread, timeline::{self, Timeline}, - ui::{self, is_compiled_as_mobile, DesktopSidePanel}, - unknowns::UnknownIds, + ui::{self, DesktopSidePanel}, + unknowns, view_state::ViewState, Result, }; +use notedeck::{Accounts, AppContext, DataPath, DataPathType, FilterState, ImageCache, UnknownIds}; + use enostr::{ClientMessage, Keypair, Pubkey, RelayEvent, RelayMessage, RelayPool}; use uuid::Uuid; -use egui::{Context, Frame, Style}; +use egui::{Frame, Style}; use egui_extras::{Size, StripBuilder}; -use nostrdb::{Config, Ndb, Transaction}; +use nostrdb::{Ndb, Transaction}; use std::collections::HashMap; use std::path::Path; @@ -48,26 +41,16 @@ pub enum DamusState { /// We derive Deserialize/Serialize so we can persist app state on shutdown. pub struct Damus { state: DamusState, - pub note_cache: NoteCache, - pub pool: RelayPool, - pub decks_cache: DecksCache, - pub ndb: Ndb, pub view_state: ViewState, - pub unknown_ids: UnknownIds, pub drafts: Drafts, pub threads: NotesHolderStorage, pub profiles: NotesHolderStorage, - pub img_cache: ImageCache, - pub accounts: Accounts, pub subscriptions: Subscriptions, - pub app_rect_handler: AppSizeHandler, pub support: Support, - pub theme: ThemeHandler, - frame_history: crate::frame_history::FrameHistory, + //frame_history: crate::frame_history::FrameHistory, - pub path: DataPath, // TODO: make these bitflags pub debug: bool, pub since_optimize: bool, @@ -99,21 +82,26 @@ fn handle_key_events(input: &egui::InputState, _pixels_per_point: f32, columns: } } -fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> { +fn try_process_event( + damus: &mut Damus, + app_ctx: &mut AppContext<'_>, + ctx: &egui::Context, +) -> Result<()> { let ppp = ctx.pixels_per_point(); - let current_columns = get_active_columns_mut(&damus.accounts, &mut damus.decks_cache); + let current_columns = get_active_columns_mut(app_ctx.accounts, &mut damus.decks_cache); ctx.input(|i| handle_key_events(i, ppp, current_columns)); let ctx2 = ctx.clone(); let wakeup = move || { ctx2.request_repaint(); }; - damus.pool.keepalive_ping(wakeup); + + app_ctx.pool.keepalive_ping(wakeup); // NOTE: we don't use the while let loop due to borrow issues #[allow(clippy::while_let_loop)] loop { - let ev = if let Some(ev) = damus.pool.try_recv() { + let ev = if let Some(ev) = app_ctx.pool.try_recv() { ev.into_owned() } else { break; @@ -121,16 +109,16 @@ fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> { match (&ev.event).into() { RelayEvent::Opened => { - damus + app_ctx .accounts - .send_initial_filters(&mut damus.pool, &ev.relay); + .send_initial_filters(app_ctx.pool, &ev.relay); timeline::send_initial_timeline_filters( - &damus.ndb, + app_ctx.ndb, damus.since_optimize, - get_active_columns_mut(&damus.accounts, &mut damus.decks_cache), + get_active_columns_mut(app_ctx.accounts, &mut damus.decks_cache), &mut damus.subscriptions, - &mut damus.pool, + app_ctx.pool, &ev.relay, ); } @@ -138,35 +126,35 @@ fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> { RelayEvent::Closed => warn!("{} connection closed", &ev.relay), RelayEvent::Error(e) => error!("{}: {}", &ev.relay, e), RelayEvent::Other(msg) => trace!("other event {:?}", &msg), - RelayEvent::Message(msg) => process_message(damus, &ev.relay, &msg), + RelayEvent::Message(msg) => process_message(damus, app_ctx, &ev.relay, &msg), } } - let current_columns = get_active_columns_mut(&damus.accounts, &mut damus.decks_cache); + let current_columns = get_active_columns_mut(app_ctx.accounts, &mut damus.decks_cache); let n_timelines = current_columns.timelines().len(); for timeline_ind in 0..n_timelines { let is_ready = { let timeline = &mut current_columns.timelines[timeline_ind]; timeline::is_timeline_ready( - &damus.ndb, - &mut damus.pool, - &mut damus.note_cache, + app_ctx.ndb, + app_ctx.pool, + app_ctx.note_cache, timeline, - &damus.accounts.mutefun(), + &app_ctx.accounts.mutefun(), ) }; if is_ready { - let txn = Transaction::new(&damus.ndb).expect("txn"); + let txn = Transaction::new(app_ctx.ndb).expect("txn"); if let Err(err) = Timeline::poll_notes_into_view( timeline_ind, current_columns.timelines_mut(), - &damus.ndb, + app_ctx.ndb, &txn, - &mut damus.unknown_ids, - &mut damus.note_cache, - &damus.accounts.mutefun(), + app_ctx.unknown_ids, + app_ctx.note_cache, + &app_ctx.accounts.mutefun(), ) { error!("poll_notes_into_view: {err}"); } @@ -175,22 +163,22 @@ fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> { } } - if damus.unknown_ids.ready_to_send() { - unknown_id_send(damus); + if app_ctx.unknown_ids.ready_to_send() { + unknown_id_send(app_ctx.unknown_ids, app_ctx.pool); } Ok(()) } -fn unknown_id_send(damus: &mut Damus) { - let filter = damus.unknown_ids.filter().expect("filter"); +fn unknown_id_send(unknown_ids: &mut UnknownIds, pool: &mut RelayPool) { + let filter = unknown_ids.filter().expect("filter"); info!( "Getting {} unknown ids from relays", - damus.unknown_ids.ids().len() + unknown_ids.ids().len() ); let msg = ClientMessage::req("unknownids".to_string(), filter); - damus.unknown_ids.clear(); - damus.pool.send(&msg); + unknown_ids.clear(); + pool.send(&msg); } #[cfg(feature = "profiling")] @@ -198,8 +186,11 @@ fn setup_profiling() { puffin::set_scopes_on(true); // tell puffin to collect data } -fn update_damus(damus: &mut Damus, ctx: &egui::Context) { - damus.accounts.update(&damus.ndb, &mut damus.pool, ctx); // update user relay and mute lists +fn update_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>) { + let _ctx = app_ctx.egui.clone(); + let ctx = &_ctx; + + app_ctx.accounts.update(app_ctx.ndb, app_ctx.pool, ctx); // update user relay and mute lists match damus.state { DamusState::Initializing => { @@ -212,10 +203,10 @@ fn update_damus(damus: &mut Damus, ctx: &egui::Context) { .subscriptions() .insert("unknownids".to_string(), SubKind::OneShot); if let Err(err) = timeline::setup_initial_nostrdb_subs( - &damus.ndb, - &mut damus.note_cache, + app_ctx.ndb, + app_ctx.note_cache, &mut damus.decks_cache, - &damus.accounts.mutefun(), + &app_ctx.accounts.mutefun(), ) { warn!("update_damus init: {err}"); } @@ -224,24 +215,27 @@ fn update_damus(damus: &mut Damus, ctx: &egui::Context) { DamusState::Initialized => (), }; - if let Err(err) = try_process_event(damus, ctx) { + if let Err(err) = try_process_event(damus, app_ctx, ctx) { error!("error processing event: {}", err); } - - damus.app_rect_handler.try_save_app_size(ctx); } -fn process_event(damus: &mut Damus, _subid: &str, event: &str) { +fn process_event(ndb: &Ndb, _subid: &str, event: &str) { #[cfg(feature = "profiling")] puffin::profile_function!(); //info!("processing event {}", event); - if let Err(_err) = damus.ndb.process_event(event) { + if let Err(_err) = ndb.process_event(event) { error!("error processing event {}", event); } } -fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) -> Result<()> { +fn handle_eose( + damus: &mut Damus, + ctx: &mut AppContext<'_>, + subid: &str, + relay_url: &str, +) -> Result<()> { let sub_kind = if let Some(sub_kind) = damus.subscriptions().get(subid) { sub_kind } else { @@ -258,29 +252,29 @@ fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) -> Result<()> { // eose on timeline? whatevs } SubKind::Initial => { - let txn = Transaction::new(&damus.ndb)?; - UnknownIds::update( + let txn = Transaction::new(ctx.ndb)?; + unknowns::update_from_columns( &txn, - &mut damus.unknown_ids, - get_active_columns(&damus.accounts, &damus.decks_cache), - &damus.ndb, - &mut damus.note_cache, + ctx.unknown_ids, + get_active_columns(ctx.accounts, &damus.decks_cache), + ctx.ndb, + ctx.note_cache, ); // this is possible if this is the first time - if damus.unknown_ids.ready_to_send() { - unknown_id_send(damus); + if ctx.unknown_ids.ready_to_send() { + unknown_id_send(ctx.unknown_ids, ctx.pool); } } // oneshot subs just close when they're done SubKind::OneShot => { let msg = ClientMessage::close(subid.to_string()); - damus.pool.send_to(&msg, relay_url); + ctx.pool.send_to(&msg, relay_url); } SubKind::FetchingContactList(timeline_uid) => { let timeline = if let Some(tl) = - get_active_columns_mut(&damus.accounts, &mut damus.decks_cache) + get_active_columns_mut(ctx.accounts, &mut damus.decks_cache) .find_timeline_mut(timeline_uid) { tl @@ -326,27 +320,28 @@ fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) -> Result<()> { Ok(()) } -fn process_message(damus: &mut Damus, relay: &str, msg: &RelayMessage) { +fn process_message(damus: &mut Damus, ctx: &mut AppContext<'_>, relay: &str, msg: &RelayMessage) { match msg { - RelayMessage::Event(subid, ev) => process_event(damus, subid, ev), + RelayMessage::Event(subid, ev) => process_event(ctx.ndb, subid, ev), RelayMessage::Notice(msg) => warn!("Notice from {}: {}", relay, msg), RelayMessage::OK(cr) => info!("OK {:?}", cr), RelayMessage::Eose(sid) => { - if let Err(err) = handle_eose(damus, sid, relay) { + if let Err(err) = handle_eose(damus, ctx, sid, relay) { error!("error handling eose: {}", err); } } } } -fn render_damus(damus: &mut Damus, ctx: &Context) { - if ui::is_narrow(ctx) { - render_damus_mobile(ctx, damus); +fn render_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>) { + if notedeck::ui::is_narrow(app_ctx.egui) { + render_damus_mobile(damus, app_ctx); } else { - render_damus_desktop(ctx, damus); + render_damus_desktop(damus, app_ctx); } - ctx.request_repaint_after(Duration::from_secs(1)); + // We use this for keeping timestamps and things up to date + app_ctx.egui.request_repaint_after(Duration::from_secs(1)); #[cfg(feature = "profiling")] puffin_egui::profiler_window(ctx); @@ -373,91 +368,12 @@ fn determine_key_storage_type() -> KeyStorageType { impl Damus { /// Called once before the first frame. - pub fn new>(ctx: &egui::Context, data_path: P, args: Vec) -> Self { + pub fn new(ctx: &mut AppContext<'_>, args: &[String]) -> Self { // arg parsing - let parsed_args = Args::parse(&args); - let is_mobile = parsed_args.is_mobile.unwrap_or(ui::is_compiled_as_mobile()); - // Some people have been running notedeck in debug, let's catch that! - if !cfg!(test) && cfg!(debug_assertions) && !parsed_args.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!("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!("---------------------------------"); - panic!(); - } - - setup_cc(ctx, is_mobile, parsed_args.light); - - let data_path = parsed_args - .datapath - .unwrap_or(data_path.as_ref().to_str().expect("db path ok").to_string()); - let path = DataPath::new(&data_path); - let dbpath_str = parsed_args - .dbpath - .unwrap_or_else(|| path.path(DataPathType::Db).to_str().unwrap().to_string()); - - let _ = std::fs::create_dir_all(&dbpath_str); - - let imgcache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir()); - let _ = std::fs::create_dir_all(imgcache_dir.clone()); - - let mapsize = if cfg!(target_os = "windows") { - // 16 Gib on windows because it actually creates the file - 1024usize * 1024usize * 1024usize * 16usize - } else { - // 1 TiB for everything else since its just virtually mapped - 1024usize * 1024usize * 1024usize * 1024usize - }; - - let theme = ThemeHandler::new(&path); - ctx.options_mut(|o| { - let cur_theme = theme.load(); - info!("Loaded theme {:?} from disk", cur_theme); - o.theme_preference = cur_theme; - }); - ctx.set_visuals_of(egui::Theme::Dark, dark_mode(is_compiled_as_mobile())); - ctx.set_visuals_of(egui::Theme::Light, light_mode()); - - let config = Config::new().set_ingester_threads(4).set_mapsize(mapsize); - - let keystore = if parsed_args.use_keystore { - let keys_path = path.path(DataPathType::Keys); - let selected_key_path = path.path(DataPathType::SelectedKey); - KeyStorageType::FileSystem(FileKeyStorage::new( - Directory::new(keys_path), - Directory::new(selected_key_path), - )) - } else { - KeyStorageType::None - }; - - let mut accounts = Accounts::new(keystore, parsed_args.relays); - - let num_keys = parsed_args.keys.len(); - - let mut unknown_ids = UnknownIds::default(); - let ndb = Ndb::new(&dbpath_str, &config).expect("ndb"); - - { - let txn = Transaction::new(&ndb).expect("txn"); - for key in parsed_args.keys { - info!("adding account: {}", key.pubkey); - accounts - .add_account(key) - .process_action(&mut unknown_ids, &ndb, &txn); - } - } - - if num_keys != 0 { - accounts.select_account(0); - } - - // AccountManager will setup the pool on first update - let pool = RelayPool::new(); - - let account = accounts + let parsed_args = ColumnsArgs::parse(args); + let account = ctx + .accounts .get_selected_account() .as_ref() .map(|a| a.pubkey.bytes()); @@ -466,19 +382,19 @@ impl Damus { info!("DecksCache: loading from command line arguments"); let mut columns: Columns = Columns::new(); for col in parsed_args.columns { - if let Some(timeline) = col.into_timeline(&ndb, account) { + if let Some(timeline) = col.into_timeline(ctx.ndb, account) { columns.add_new_timeline_column(timeline); } } columns_to_decks_cache(columns, account) - } else if let Some(decks_cache) = storage::load_decks_cache(&path, &ndb) { + } else if let Some(decks_cache) = crate::storage::load_decks_cache(ctx.path, ctx.ndb) { info!( "DecksCache: loading from disk {}", crate::storage::DECKS_CACHE_FILE ); decks_cache - } else if let Some(cols) = storage::deserialize_columns(&path, &ndb, account) { + } else if let Some(cols) = storage::deserialize_columns(ctx.path, ctx.ndb, account) { info!( "DecksCache: loading from disk at depreciated location {}", crate::storage::COLUMNS_FILE @@ -486,79 +402,40 @@ impl Damus { columns_to_decks_cache(cols, account) } else { info!("DecksCache: creating new with demo configuration"); - let mut cache = DecksCache::new_with_demo_config(&ndb); - for account in accounts.get_accounts() { + let mut cache = DecksCache::new_with_demo_config(ctx.ndb); + for account in ctx.accounts.get_accounts() { cache.add_deck_default(account.pubkey); } - set_demo(&mut cache, &ndb, &mut accounts, &mut unknown_ids); + set_demo(&mut cache, ctx.ndb, ctx.accounts, ctx.unknown_ids); cache }; - let debug = parsed_args.debug; - - let app_rect_handler = AppSizeHandler::new(&path); - let support = Support::new(&path); + let debug = ctx.args.debug; + let support = Support::new(ctx.path); Self { - pool, - debug, - unknown_ids, subscriptions: Subscriptions::default(), since_optimize: parsed_args.since_optimize, threads: NotesHolderStorage::default(), profiles: NotesHolderStorage::default(), drafts: Drafts::default(), state: DamusState::Initializing, - img_cache: ImageCache::new(imgcache_dir), - note_cache: NoteCache::default(), textmode: parsed_args.textmode, - ndb, - accounts, - frame_history: FrameHistory::default(), + //frame_history: FrameHistory::default(), view_state: ViewState::default(), - path, - app_rect_handler, support, decks_cache, - theme, + debug, } } - pub fn pool_mut(&mut self) -> &mut RelayPool { - &mut self.pool + pub fn columns_mut(&mut self, accounts: &Accounts) -> &mut Columns { + get_active_columns_mut(accounts, &mut self.decks_cache) } - pub fn ndb(&self) -> &Ndb { - &self.ndb - } - - pub fn drafts_mut(&mut self) -> &mut Drafts { - &mut self.drafts - } - - pub fn img_cache_mut(&mut self) -> &mut ImageCache { - &mut self.img_cache - } - - pub fn accounts(&self) -> &Accounts { - &self.accounts - } - - pub fn accounts_mut(&mut self) -> &mut Accounts { - &mut self.accounts - } - - pub fn view_state_mut(&mut self) -> &mut ViewState { - &mut self.view_state - } - - pub fn columns_mut(&mut self) -> &mut Columns { - get_active_columns_mut(&self.accounts, &mut self.decks_cache) - } - - pub fn columns(&self) -> &Columns { - get_active_columns(&self.accounts, &self.decks_cache) + pub fn columns(&self, accounts: &Accounts) -> &Columns { + get_active_columns(accounts, &self.decks_cache) } pub fn gen_subid(&self, kind: &SubKind) -> String { @@ -573,44 +450,25 @@ impl Damus { let decks_cache = DecksCache::default(); let path = DataPath::new(&data_path); - let theme = ThemeHandler::new(&path); let imgcache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir()); let _ = std::fs::create_dir_all(imgcache_dir.clone()); let debug = true; - let app_rect_handler = AppSizeHandler::new(&path); let support = Support::new(&path); - let config = Config::new().set_ingester_threads(2); - Self { debug, - unknown_ids: UnknownIds::default(), subscriptions: Subscriptions::default(), since_optimize: true, threads: NotesHolderStorage::default(), profiles: NotesHolderStorage::default(), drafts: Drafts::default(), state: DamusState::Initializing, - pool: RelayPool::new(), - img_cache: ImageCache::new(imgcache_dir), - note_cache: NoteCache::default(), textmode: false, - ndb: Ndb::new( - path.path(DataPathType::Db) - .to_str() - .expect("db path should be ok"), - &config, - ) - .expect("ndb"), - accounts: Accounts::new(KeyStorageType::None, vec![]), - frame_history: FrameHistory::default(), + //frame_history: FrameHistory::default(), view_state: ViewState::default(), - path, - app_rect_handler, support, decks_cache, - theme, } } @@ -618,14 +476,6 @@ impl Damus { &mut self.subscriptions.subs } - pub fn note_cache_mut(&mut self) -> &mut NoteCache { - &mut self.note_cache - } - - pub fn unknown_ids_mut(&mut self) -> &mut UnknownIds { - &mut self.unknown_ids - } - pub fn threads(&self) -> &NotesHolderStorage { &self.threads } @@ -633,10 +483,6 @@ impl Damus { pub fn threads_mut(&mut self) -> &mut NotesHolderStorage { &mut self.threads } - - pub fn note_cache(&self) -> &NoteCache { - &self.note_cache - } } /* @@ -648,17 +494,20 @@ fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) { } */ -fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) { +fn render_damus_mobile(app: &mut Damus, app_ctx: &mut AppContext<'_>) { + let _ctx = app_ctx.egui.clone(); + let ctx = &_ctx; + #[cfg(feature = "profiling")] puffin::profile_function!(); //let routes = app.timelines[0].routes.clone(); - main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| { - if !app.columns().columns().is_empty() - && nav::render_nav(0, app, ui).process_render_nav_response(app) + main_panel(&ctx.style(), notedeck::ui::is_narrow(ctx)).show(ctx, |ui| { + if !app.columns(app_ctx.accounts).columns().is_empty() + && nav::render_nav(0, app, app_ctx, ui).process_render_nav_response(app, app_ctx) { - storage::save_decks_cache(&app.path, &app.decks_cache); + storage::save_decks_cache(app_ctx.path, &app.decks_cache); } }); } @@ -677,13 +526,16 @@ fn main_panel(style: &Style, narrow: bool) -> egui::CentralPanel { }) } -fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) { +fn render_damus_desktop(app: &mut Damus, app_ctx: &mut AppContext<'_>) { + let _ctx = app_ctx.egui.clone(); + let ctx = &_ctx; + #[cfg(feature = "profiling")] puffin::profile_function!(); let screen_size = ctx.screen_rect().width(); let calc_panel_width = (screen_size - / get_active_columns(&app.accounts, &app.decks_cache).num_columns() as f32) + / get_active_columns(app_ctx.accounts, &app.decks_cache).num_columns() as f32) - 30.0; let min_width = 320.0; let need_scroll = calc_panel_width < min_width; @@ -693,24 +545,24 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) { Size::remainder() }; - main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| { + main_panel(&ctx.style(), notedeck::ui::is_narrow(ctx)).show(ctx, |ui| { ui.spacing_mut().item_spacing.x = 0.0; if need_scroll { egui::ScrollArea::horizontal().show(ui, |ui| { - timelines_view(ui, panel_sizes, app); + timelines_view(ui, panel_sizes, app, app_ctx); }); } else { - timelines_view(ui, panel_sizes, app); + timelines_view(ui, panel_sizes, app, app_ctx); } }); } -fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) { +fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, ctx: &mut AppContext<'_>) { StripBuilder::new(ui) .size(Size::exact(ui::side_panel::SIDE_PANEL_WIDTH)) .sizes( sizes, - get_active_columns(&app.accounts, &app.decks_cache).num_columns(), + get_active_columns(ctx.accounts, &app.decks_cache).num_columns(), ) .clip(true) .horizontal(|mut strip| { @@ -718,9 +570,9 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) { strip.cell(|ui| { let rect = ui.available_rect_before_wrap(); let side_panel = DesktopSidePanel::new( - &app.ndb, - &mut app.img_cache, - app.accounts.get_selected_account(), + ctx.ndb, + ctx.img_cache, + ctx.accounts.get_selected_account(), &app.decks_cache, ) .show(ui); @@ -728,9 +580,9 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) { if side_panel.response.clicked() || side_panel.response.secondary_clicked() { if let Some(action) = DesktopSidePanel::perform_action( &mut app.decks_cache, - &app.accounts, + ctx.accounts, &mut app.support, - &mut app.theme, + ctx.theme, side_panel.action, ) { side_panel_action = Some(action); @@ -747,15 +599,15 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) { let mut save_cols = false; if let Some(action) = side_panel_action { - save_cols = save_cols || action.process(app); + save_cols = save_cols || action.process(app, ctx); } - let num_cols = app.columns().num_columns(); + let num_cols = app.columns(ctx.accounts).num_columns(); let mut responses = Vec::with_capacity(num_cols); for col_index in 0..num_cols { strip.cell(|ui| { let rect = ui.available_rect_before_wrap(); - responses.push(nav::render_nav(col_index, app, ui)); + responses.push(nav::render_nav(col_index, app, ctx, ui)); // vertical line ui.painter().vline( @@ -769,27 +621,23 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) { } for response in responses { - let save = response.process_render_nav_response(app); + let save = response.process_render_nav_response(app, ctx); save_cols = save_cols || save; } if save_cols { - storage::save_decks_cache(&app.path, &app.decks_cache); + storage::save_decks_cache(ctx.path, &app.decks_cache); } }); } -impl eframe::App for Damus { - /// Called by the frame work to save state before shutdown. - fn save(&mut self, _storage: &mut dyn eframe::Storage) { - //eframe::set_value(storage, eframe::APP_KEY, self); - } - - /// Called each time the UI needs repainting, which may be many times per second. - /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. - fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - self.frame_history +impl notedeck::App for Damus { + fn update(&mut self, ctx: &mut AppContext<'_>) { + /* + self.app + .frame_history .on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage); + */ #[cfg(feature = "profiling")] puffin::GlobalProfiler::lock().new_frame(); diff --git a/crates/notedeck_columns/src/app_creation.rs b/crates/notedeck_columns/src/app_creation.rs index 425f96d..8b13789 100644 --- a/crates/notedeck_columns/src/app_creation.rs +++ b/crates/notedeck_columns/src/app_creation.rs @@ -1,83 +1 @@ -use crate::{ - app_size_handler::AppSizeHandler, - app_style::{add_custom_style, dark_mode, light_mode}, - fonts::setup_fonts, - storage::DataPath, -}; -use eframe::NativeOptions; - -//pub const UI_SCALE_FACTOR: f32 = 0.2; - -pub fn generate_native_options(paths: DataPath) -> NativeOptions { - let window_builder = Box::new(move |builder: egui::ViewportBuilder| { - let builder = builder - .with_fullsize_content_view(true) - .with_titlebar_shown(false) - .with_title_shown(false) - .with_icon(std::sync::Arc::new( - eframe::icon_data::from_png_bytes(app_icon()).expect("icon"), - )); - - if let Some(window_size) = AppSizeHandler::new(&paths).get_app_size() { - builder.with_inner_size(window_size) - } else { - builder - } - }); - - eframe::NativeOptions { - window_builder: Some(window_builder), - viewport: egui::ViewportBuilder::default().with_icon(std::sync::Arc::new( - eframe::icon_data::from_png_bytes(app_icon()).expect("icon"), - )), - ..Default::default() - } -} - -fn generate_native_options_with_builder_modifiers( - apply_builder_modifiers: fn(egui::ViewportBuilder) -> egui::ViewportBuilder, -) -> NativeOptions { - let window_builder = - Box::new(move |builder: egui::ViewportBuilder| apply_builder_modifiers(builder)); - - eframe::NativeOptions { - window_builder: Some(window_builder), - ..Default::default() - } -} - -pub fn app_icon() -> &'static [u8; 271986] { - std::include_bytes!("../assets/damus-app-icon.png") -} - -pub fn generate_mobile_emulator_native_options() -> eframe::NativeOptions { - generate_native_options_with_builder_modifiers(|builder| { - builder - .with_fullsize_content_view(true) - .with_titlebar_shown(false) - .with_title_shown(false) - .with_inner_size([405.0, 915.0]) - .with_icon(eframe::icon_data::from_png_bytes(app_icon()).expect("icon")) - }) -} - -pub fn setup_cc(ctx: &egui::Context, is_mobile: bool, light: bool) { - setup_fonts(ctx); - - //ctx.set_pixels_per_point(ctx.pixels_per_point() + UI_SCALE_FACTOR); - //ctx.set_pixels_per_point(1.0); - // - // - //ctx.tessellation_options_mut(|to| to.feathering = false); - - egui_extras::install_image_loaders(ctx); - - if light { - ctx.set_visuals(light_mode()) - } else { - ctx.set_visuals(dark_mode(is_mobile)); - } - - ctx.all_styles_mut(|style| add_custom_style(is_mobile, style)); -} diff --git a/crates/notedeck_columns/src/app_style.rs b/crates/notedeck_columns/src/app_style.rs index 6d12cf5..7e9626b 100644 --- a/crates/notedeck_columns/src/app_style.rs +++ b/crates/notedeck_columns/src/app_style.rs @@ -1,210 +1,6 @@ -use crate::{ - colors::{desktop_dark_color_theme, light_color_theme, mobile_dark_color_theme, ColorTheme}, - fonts::NamedFontFamily, - ui::is_narrow, -}; -use egui::{ - epaint::Shadow, - style::{Interaction, Selection, WidgetVisuals, Widgets}, - FontFamily, FontId, Rounding, Stroke, Style, TextStyle, Visuals, -}; -use strum::IntoEnumIterator; -use strum_macros::EnumIter; +use egui::{FontFamily, FontId}; -const WIDGET_ROUNDING: Rounding = Rounding::same(8.0); - -pub fn light_mode() -> Visuals { - create_themed_visuals(light_color_theme(), Visuals::light()) -} - -pub fn dark_mode(mobile: bool) -> Visuals { - create_themed_visuals( - if mobile { - mobile_dark_color_theme() - } else { - desktop_dark_color_theme() - }, - Visuals::dark(), - ) -} - -/// Create custom text sizes for any FontSizes -pub fn add_custom_style(is_mobile: bool, style: &mut Style) { - let font_size = if is_mobile { - mobile_font_size - } else { - desktop_font_size - }; - style.text_styles = NotedeckTextStyle::iter() - .map(|text_style| { - ( - text_style.text_style(), - FontId::new(font_size(&text_style), text_style.font_family()), - ) - }) - .collect(); - - style.interaction = Interaction { - tooltip_delay: 0.1, - show_tooltips_only_when_still: false, - ..Interaction::default() - }; - - #[cfg(debug_assertions)] - { - style.debug.show_interactive_widgets = true; - style.debug.debug_on_hover_with_all_modifiers = true; - } -} - -pub fn desktop_font_size(text_style: &NotedeckTextStyle) -> f32 { - match text_style { - NotedeckTextStyle::Heading => 48.0, - NotedeckTextStyle::Heading2 => 24.0, - NotedeckTextStyle::Heading3 => 20.0, - NotedeckTextStyle::Heading4 => 14.0, - NotedeckTextStyle::Body => 16.0, - NotedeckTextStyle::Monospace => 13.0, - NotedeckTextStyle::Button => 13.0, - NotedeckTextStyle::Small => 12.0, - NotedeckTextStyle::Tiny => 10.0, - } -} - -pub fn mobile_font_size(text_style: &NotedeckTextStyle) -> f32 { - // TODO: tweak text sizes for optimal mobile viewing - match text_style { - NotedeckTextStyle::Heading => 48.0, - NotedeckTextStyle::Heading2 => 24.0, - NotedeckTextStyle::Heading3 => 20.0, - NotedeckTextStyle::Heading4 => 14.0, - NotedeckTextStyle::Body => 13.0, - NotedeckTextStyle::Monospace => 13.0, - NotedeckTextStyle::Button => 13.0, - NotedeckTextStyle::Small => 12.0, - NotedeckTextStyle::Tiny => 10.0, - } -} - -pub fn get_font_size(ctx: &egui::Context, text_style: &NotedeckTextStyle) -> f32 { - if is_narrow(ctx) { - mobile_font_size(text_style) - } else { - desktop_font_size(text_style) - } -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug, EnumIter)] -pub enum NotedeckTextStyle { - Heading, - Heading2, - Heading3, - Heading4, - Body, - Monospace, - Button, - Small, - Tiny, -} - -impl NotedeckTextStyle { - pub fn text_style(&self) -> TextStyle { - match self { - Self::Heading => TextStyle::Heading, - Self::Heading2 => TextStyle::Name("Heading2".into()), - Self::Heading3 => TextStyle::Name("Heading3".into()), - Self::Heading4 => TextStyle::Name("Heading4".into()), - Self::Body => TextStyle::Body, - Self::Monospace => TextStyle::Monospace, - Self::Button => TextStyle::Button, - Self::Small => TextStyle::Small, - Self::Tiny => TextStyle::Name("Tiny".into()), - } - } - - pub fn font_family(&self) -> FontFamily { - match self { - Self::Heading => FontFamily::Proportional, - Self::Heading2 => FontFamily::Proportional, - Self::Heading3 => FontFamily::Proportional, - Self::Heading4 => FontFamily::Proportional, - Self::Body => FontFamily::Proportional, - Self::Monospace => FontFamily::Monospace, - Self::Button => FontFamily::Proportional, - Self::Small => FontFamily::Proportional, - Self::Tiny => FontFamily::Proportional, - } - } -} - -pub fn create_themed_visuals(theme: ColorTheme, default: Visuals) -> Visuals { - Visuals { - hyperlink_color: theme.hyperlink_color, - override_text_color: Some(theme.text_color), - panel_fill: theme.panel_fill, - selection: Selection { - bg_fill: theme.selection_color, - stroke: Stroke { - width: 1.0, - color: theme.selection_color, - }, - }, - warn_fg_color: theme.warn_fg_color, - widgets: Widgets { - noninteractive: WidgetVisuals { - bg_fill: theme.noninteractive_bg_fill, - weak_bg_fill: theme.noninteractive_weak_bg_fill, - bg_stroke: Stroke { - width: 1.0, - color: theme.noninteractive_bg_stroke_color, - }, - fg_stroke: Stroke { - width: 1.0, - color: theme.noninteractive_fg_stroke_color, - }, - rounding: WIDGET_ROUNDING, - ..default.widgets.noninteractive - }, - inactive: WidgetVisuals { - bg_fill: theme.inactive_bg_fill, - weak_bg_fill: theme.inactive_weak_bg_fill, - bg_stroke: Stroke { - width: 1.0, - color: theme.inactive_bg_stroke_color, - }, - rounding: WIDGET_ROUNDING, - ..default.widgets.inactive - }, - hovered: WidgetVisuals { - rounding: WIDGET_ROUNDING, - ..default.widgets.hovered - }, - active: WidgetVisuals { - rounding: WIDGET_ROUNDING, - ..default.widgets.active - }, - open: WidgetVisuals { - ..default.widgets.open - }, - }, - extreme_bg_color: theme.extreme_bg_color, - error_fg_color: theme.err_fg_color, - window_rounding: Rounding::same(8.0), - window_fill: theme.window_fill, - window_shadow: Shadow { - offset: [0.0, 8.0].into(), - blur: 24.0, - spread: 0.0, - color: egui::Color32::from_rgba_unmultiplied(0x6D, 0x6D, 0x6D, 0x14), - }, - window_stroke: Stroke { - width: 1.0, - color: theme.window_stroke_color, - }, - image_loading_spinners: false, - ..default - } -} +use notedeck::fonts::NamedFontFamily; pub static DECK_ICON_SIZE: f32 = 24.0; diff --git a/crates/notedeck_columns/src/args.rs b/crates/notedeck_columns/src/args.rs index 43a39f1..88ce805 100644 --- a/crates/notedeck_columns/src/args.rs +++ b/crates/notedeck_columns/src/args.rs @@ -1,37 +1,22 @@ -use crate::filter::FilterState; +use notedeck::FilterState; + use crate::timeline::{PubkeySource, Timeline, TimelineKind}; -use enostr::{Filter, Keypair, Pubkey, SecretKey}; +use enostr::{Filter, Pubkey}; use nostrdb::Ndb; use tracing::{debug, error, info}; -pub struct Args { +pub struct ColumnsArgs { pub columns: Vec, - pub relays: Vec, - pub is_mobile: Option, - pub keys: Vec, pub since_optimize: bool, - pub light: bool, - pub debug: bool, pub textmode: bool, - pub use_keystore: bool, - pub dbpath: Option, - pub datapath: Option, } -impl Args { +impl ColumnsArgs { pub fn parse(args: &[String]) -> Self { - let mut res = Args { + let mut res = Self { columns: vec![], - relays: vec![], - is_mobile: None, - keys: vec![], - light: false, since_optimize: true, - debug: false, textmode: false, - use_keystore: true, - dbpath: None, - datapath: None, }; let mut i = 0; @@ -39,50 +24,8 @@ impl Args { while i < len { let arg = &args[i]; - if arg == "--mobile" { - res.is_mobile = Some(true); - } else if arg == "--light" { - res.light = true; - } else if arg == "--dark" { - res.light = false; - } else if arg == "--debug" { - res.debug = true; - } else if arg == "--textmode" { + if arg == "--textmode" { res.textmode = true; - } else if arg == "--pub" || arg == "--npub" { - i += 1; - let pubstr = if let Some(next_arg) = args.get(i) { - next_arg - } else { - error!("sec argument missing?"); - continue; - }; - - if let Ok(pk) = Pubkey::parse(pubstr) { - res.keys.push(Keypair::only_pubkey(pk)); - } else { - error!( - "failed to parse {} argument. Make sure to use hex or npub.", - arg - ); - } - } else if arg == "--sec" || arg == "--nsec" { - i += 1; - let secstr = if let Some(next_arg) = args.get(i) { - next_arg - } else { - error!("sec argument missing?"); - continue; - }; - - if let Ok(sec) = SecretKey::parse(secstr) { - res.keys.push(Keypair::from_secret(sec)); - } else { - error!( - "failed to parse {} argument. Make sure to use hex or nsec.", - arg - ); - } } else if arg == "--no-since-optimize" { res.since_optimize = false; } else if arg == "--filter" { @@ -99,33 +42,6 @@ impl Args { } else { error!("failed to parse filter '{}'", filter); } - } else if arg == "--dbpath" { - i += 1; - let path = if let Some(next_arg) = args.get(i) { - next_arg - } else { - error!("dbpath argument missing?"); - continue; - }; - res.dbpath = Some(path.clone()); - } else if arg == "--datapath" { - i += 1; - let path = if let Some(next_arg) = args.get(i) { - next_arg - } else { - error!("datapath argument missing?"); - continue; - }; - res.datapath = Some(path.clone()); - } else if arg == "-r" || arg == "--relay" { - i += 1; - let relay = if let Some(next_arg) = args.get(i) { - next_arg - } else { - error!("relay argument missing?"); - continue; - }; - res.relays.push(relay.clone()); } else if arg == "--column" || arg == "-c" { i += 1; let column_name = if let Some(next_arg) = args.get(i) { @@ -212,8 +128,6 @@ impl Args { } else { error!("failed to parse filter in '{}'", filter_file); } - } else if arg == "--no-keystore" { - res.use_keystore = false; } i += 1; @@ -242,82 +156,3 @@ impl ArgColumn { } } } - -#[cfg(test)] -mod tests { - use crate::app::Damus; - - use std::path::{Path, PathBuf}; - - fn create_tmp_dir() -> PathBuf { - tempfile::TempDir::new() - .expect("tmp path") - .path() - .to_path_buf() - } - - fn rmrf(path: impl AsRef) { - let _ = std::fs::remove_dir_all(path); - } - - /// Ensure dbpath actually sets the dbpath correctly. - #[tokio::test] - async fn test_dbpath() { - let datapath = create_tmp_dir(); - let dbpath = create_tmp_dir(); - let args = vec![ - "--datapath", - &datapath.to_str().unwrap(), - "--dbpath", - &dbpath.to_str().unwrap(), - ] - .iter() - .map(|s| s.to_string()) - .collect(); - - let ctx = egui::Context::default(); - let _app = Damus::new(&ctx, &datapath, args); - - assert!(Path::new(&dbpath.join("data.mdb")).exists()); - assert!(Path::new(&dbpath.join("lock.mdb")).exists()); - assert!(!Path::new(&datapath.join("db")).exists()); - - rmrf(datapath); - rmrf(dbpath); - } - - #[tokio::test] - async fn test_column_args() { - let tmpdir = create_tmp_dir(); - let npub = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s"; - let args = vec![ - "--no-keystore", - "--pub", - npub, - "-c", - "notifications", - "-c", - "contacts", - ] - .iter() - .map(|s| s.to_string()) - .collect(); - - let ctx = egui::Context::default(); - let app = Damus::new(&ctx, &tmpdir, args); - - assert_eq!(app.columns().columns().len(), 2); - - let tl1 = app.columns().column(0).router().top().timeline_id(); - let tl2 = app.columns().column(1).router().top().timeline_id(); - - assert_eq!(tl1.is_some(), true); - assert_eq!(tl2.is_some(), true); - - let timelines = app.columns().timelines(); - assert!(timelines[0].kind.is_notifications()); - assert!(timelines[1].kind.is_contacts()); - - rmrf(tmpdir); - } -} diff --git a/crates/notedeck_columns/src/colors.rs b/crates/notedeck_columns/src/colors.rs index b33ca30..ef3b1d8 100644 --- a/crates/notedeck_columns/src/colors.rs +++ b/crates/notedeck_columns/src/colors.rs @@ -1,115 +1,5 @@ use egui::Color32; -pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5); -const PURPLE_ALT: Color32 = Color32::from_rgb(0x82, 0x56, 0xDD); -// TODO: This should not be exposed publicly -pub const PINK: Color32 = Color32::from_rgb(0xE4, 0x5A, 0xC9); -//pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52); -pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A); -const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00); -const RED_700: Color32 = Color32::from_rgb(0xC7, 0x37, 0x5A); -const ORANGE_700: Color32 = Color32::from_rgb(0xF6, 0xB1, 0x4A); - -// BACKGROUNDS -const SEMI_DARKER_BG: Color32 = Color32::from_rgb(0x39, 0x39, 0x39); -const DARKER_BG: Color32 = Color32::from_rgb(0x1F, 0x1F, 0x1F); -const DARK_BG: Color32 = Color32::from_rgb(0x2C, 0x2C, 0x2C); -const DARK_ISH_BG: Color32 = Color32::from_rgb(0x25, 0x25, 0x25); -const SEMI_DARK_BG: Color32 = Color32::from_rgb(0x44, 0x44, 0x44); - -const LIGHTER_GRAY: Color32 = Color32::from_rgb(0xf8, 0xf8, 0xf8); -const LIGHT_GRAY: Color32 = Color32::from_rgb(0xc8, 0xc8, 0xc8); // 78% -pub const MID_GRAY: Color32 = Color32::from_rgb(0xbd, 0xbd, 0xbd); -const DARKER_GRAY: Color32 = Color32::from_rgb(0xa5, 0xa5, 0xa5); // 65% -const EVEN_DARKER_GRAY: Color32 = Color32::from_rgb(0x89, 0x89, 0x89); // 54% pub const ALMOST_WHITE: Color32 = Color32::from_rgb(0xFA, 0xFA, 0xFA); - -pub struct ColorTheme { - // VISUALS - pub panel_fill: Color32, - pub extreme_bg_color: Color32, - pub text_color: Color32, - pub err_fg_color: Color32, - pub warn_fg_color: Color32, - pub hyperlink_color: Color32, - pub selection_color: Color32, - - // WINDOW - pub window_fill: Color32, - pub window_stroke_color: Color32, - - // NONINTERACTIVE WIDGET - pub noninteractive_bg_fill: Color32, - pub noninteractive_weak_bg_fill: Color32, - pub noninteractive_bg_stroke_color: Color32, - pub noninteractive_fg_stroke_color: Color32, - - // INACTIVE WIDGET - pub inactive_bg_stroke_color: Color32, - pub inactive_bg_fill: Color32, - pub inactive_weak_bg_fill: Color32, -} - -pub fn desktop_dark_color_theme() -> ColorTheme { - ColorTheme { - // VISUALS - panel_fill: DARKER_BG, - extreme_bg_color: DARK_ISH_BG, - text_color: Color32::WHITE, - err_fg_color: RED_700, - warn_fg_color: ORANGE_700, - hyperlink_color: PURPLE, - selection_color: PURPLE_ALT, - - // WINDOW - window_fill: DARK_ISH_BG, - window_stroke_color: DARK_BG, - - // NONINTERACTIVE WIDGET - noninteractive_bg_fill: DARK_ISH_BG, - noninteractive_weak_bg_fill: DARK_BG, - noninteractive_bg_stroke_color: SEMI_DARKER_BG, - noninteractive_fg_stroke_color: GRAY_SECONDARY, - - // INACTIVE WIDGET - inactive_bg_stroke_color: SEMI_DARKER_BG, - inactive_bg_fill: Color32::from_rgb(0x25, 0x25, 0x25), - inactive_weak_bg_fill: SEMI_DARK_BG, - } -} - -pub fn mobile_dark_color_theme() -> ColorTheme { - ColorTheme { - panel_fill: Color32::BLACK, - noninteractive_weak_bg_fill: Color32::from_rgb(0x1F, 0x1F, 0x1F), - ..desktop_dark_color_theme() - } -} - -pub fn light_color_theme() -> ColorTheme { - ColorTheme { - // VISUALS - panel_fill: Color32::WHITE, - extreme_bg_color: LIGHTER_GRAY, - text_color: BLACK, - err_fg_color: RED_700, - warn_fg_color: ORANGE_700, - hyperlink_color: PURPLE, - selection_color: PURPLE_ALT, - - // WINDOW - window_fill: Color32::WHITE, - window_stroke_color: DARKER_GRAY, - - // NONINTERACTIVE WIDGET - noninteractive_bg_fill: Color32::WHITE, - noninteractive_weak_bg_fill: LIGHTER_GRAY, - noninteractive_bg_stroke_color: LIGHT_GRAY, - noninteractive_fg_stroke_color: GRAY_SECONDARY, - - // INACTIVE WIDGET - inactive_bg_stroke_color: EVEN_DARKER_GRAY, - inactive_bg_fill: LIGHT_GRAY, - inactive_weak_bg_fill: EVEN_DARKER_GRAY, - } -} +pub const MID_GRAY: Color32 = Color32::from_rgb(0xbd, 0xbd, 0xbd); +pub const PINK: Color32 = Color32::from_rgb(0xE4, 0x5A, 0xC9); diff --git a/crates/notedeck_columns/src/error.rs b/crates/notedeck_columns/src/error.rs index 371aa04..b00b28d 100644 --- a/crates/notedeck_columns/src/error.rs +++ b/crates/notedeck_columns/src/error.rs @@ -1,126 +1,31 @@ -use std::{fmt, io}; +use std::io; -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub enum FilterError { - EmptyContactList, -} - -#[derive(Debug, Eq, PartialEq, Copy, Clone)] -pub enum SubscriptionError { - //#[error("No active subscriptions")] - NoActive, - - /// When a timeline has an unexpected number - /// of active subscriptions. Should only happen if there - /// is a bug in notedeck - //#[error("Unexpected subscription count")] - UnexpectedSubscriptionCount(i32), -} - -impl Error { - pub fn unexpected_sub_count(c: i32) -> Self { - Error::SubscriptionError(SubscriptionError::UnexpectedSubscriptionCount(c)) - } - - pub fn no_active_sub() -> Self { - Error::SubscriptionError(SubscriptionError::NoActive) - } -} - -impl fmt::Display for SubscriptionError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NoActive => write!(f, "No active subscriptions"), - Self::UnexpectedSubscriptionCount(c) => { - write!(f, "Unexpected subscription count: {}", c) - } - } - } -} - -#[derive(Debug)] +#[derive(thiserror::Error, Debug)] pub enum Error { + #[error("timeline not found")] TimelineNotFound, + + #[error("load failed")] LoadFailed, - SubscriptionError(SubscriptionError), - Filter(FilterError), - Io(io::Error), - Nostr(enostr::Error), - Ndb(nostrdb::Error), - Image(image::error::ImageError), + + #[error("network error: {0}")] + Nostr(#[from] enostr::Error), + + #[error("database error: {0}")] + Ndb(#[from] nostrdb::Error), + + #[error("io error: {0}")] + Io(#[from] io::Error), + + #[error("notedeck app error: {0}")] + App(#[from] notedeck::Error), + + #[error("generic error: {0}")] Generic(String), } -impl Error { - pub fn empty_contact_list() -> Self { - Error::Filter(FilterError::EmptyContactList) - } -} - -impl fmt::Display for FilterError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::EmptyContactList => { - write!(f, "empty contact list") - } - } - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::SubscriptionError(e) => { - write!(f, "{e}") - } - Self::TimelineNotFound => write!(f, "Timeline not found"), - Self::LoadFailed => { - write!(f, "load failed") - } - Self::Filter(e) => { - write!(f, "{e}") - } - Self::Nostr(e) => write!(f, "{e}"), - Self::Ndb(e) => write!(f, "{e}"), - Self::Image(e) => write!(f, "{e}"), - Self::Generic(e) => write!(f, "{e}"), - Self::Io(e) => write!(f, "{e}"), - } - } -} - impl From for Error { fn from(s: String) -> Self { Error::Generic(s) } } - -impl From for Error { - fn from(e: nostrdb::Error) -> Self { - Error::Ndb(e) - } -} - -impl From for Error { - fn from(err: image::error::ImageError) -> Self { - Error::Image(err) - } -} - -impl From for Error { - fn from(err: enostr::Error) -> Self { - Error::Nostr(err) - } -} - -impl From for Error { - fn from(err: io::Error) -> Self { - Error::Io(err) - } -} - -impl From for Error { - fn from(err: FilterError) -> Self { - Error::Filter(err) - } -} diff --git a/crates/notedeck_columns/src/frame_history.rs b/crates/notedeck_columns/src/frame_history.rs index b364622..01e9492 100644 --- a/crates/notedeck_columns/src/frame_history.rs +++ b/crates/notedeck_columns/src/frame_history.rs @@ -1,3 +1,4 @@ +/* use egui::util::History; pub struct FrameHistory { @@ -46,3 +47,4 @@ impl FrameHistory { egui::warn_if_debug_build(ui); } } +*/ diff --git a/crates/notedeck_columns/src/images.rs b/crates/notedeck_columns/src/images.rs index b507f4c..8092178 100644 --- a/crates/notedeck_columns/src/images.rs +++ b/crates/notedeck_columns/src/images.rs @@ -1,8 +1,7 @@ -use crate::error::Error; -use crate::imgcache::ImageCache; -use crate::result::Result; use egui::{pos2, Color32, ColorImage, Rect, Sense, SizeHint, TextureHandle}; use image::imageops::FilterType; +use notedeck::ImageCache; +use notedeck::Result; use poll_promise::Promise; use std::path; use tokio::fs; @@ -183,7 +182,7 @@ fn fetch_img_from_disk( let path = path.to_owned(); Promise::spawn_async(async move { let data = fs::read(path).await?; - let image_buffer = image::load_from_memory(&data)?; + let image_buffer = image::load_from_memory(&data).map_err(notedeck::Error::Image)?; // TODO: remove unwrap here let flat_samples = image_buffer.as_flat_samples_u8().unwrap(); @@ -239,7 +238,7 @@ fn fetch_img_from_net( let cache_path = cache_path.to_owned(); ehttp::fetch(request, move |response| { let handle = response - .map_err(Error::Generic) + .map_err(notedeck::Error::Generic) .and_then(|resp| parse_img_response(resp, imgtyp)) .map(|img| { let texture_handle = ctx.load_texture(&cloned_url, img.clone(), Default::default()); diff --git a/crates/notedeck_columns/src/lib.rs b/crates/notedeck_columns/src/lib.rs index b1ad0e4..ecbe092 100644 --- a/crates/notedeck_columns/src/lib.rs +++ b/crates/notedeck_columns/src/lib.rs @@ -7,7 +7,6 @@ mod abbrev; pub mod accounts; mod actionbar; pub mod app_creation; -mod app_size_handler; mod app_style; mod args; mod colors; @@ -15,35 +14,24 @@ mod column; mod deck_state; mod decks; mod draft; -mod filter; -mod fonts; mod frame_history; mod images; -mod imgcache; mod key_parsing; pub mod login_manager; mod multi_subscriber; -mod muted; mod nav; -mod note; -mod notecache; mod notes_holder; mod post; mod profile; pub mod relay_pool_manager; -mod result; mod route; mod subscriptions; mod support; mod test_data; -mod theme_handler; mod thread; -mod time; -mod timecache; mod timeline; pub mod ui; mod unknowns; -mod user_account; mod view_state; #[cfg(test)] diff --git a/crates/notedeck_columns/src/multi_subscriber.rs b/crates/notedeck_columns/src/multi_subscriber.rs index a747be3..27fb952 100644 --- a/crates/notedeck_columns/src/multi_subscriber.rs +++ b/crates/notedeck_columns/src/multi_subscriber.rs @@ -3,7 +3,8 @@ use nostrdb::{Ndb, Note, Transaction}; use tracing::{debug, error, info}; use uuid::Uuid; -use crate::{filter::UnifiedSubscription, muted::MuteFun, note::NoteRef, Error}; +use crate::Error; +use notedeck::{MuteFun, NoteRef, UnifiedSubscription}; pub struct MultiSubscriber { filters: Vec, @@ -111,7 +112,7 @@ impl MultiSubscriber { txn: &Transaction, is_muted: &MuteFun, ) -> Result, Error> { - let sub = self.sub.as_ref().ok_or(Error::no_active_sub())?; + let sub = self.sub.as_ref().ok_or(notedeck::Error::no_active_sub())?; let new_note_keys = ndb.poll_for_notes(sub.local, 500); if new_note_keys.is_empty() { diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs index eb788e0..127ef6f 100644 --- a/crates/notedeck_columns/src/nav.rs +++ b/crates/notedeck_columns/src/nav.rs @@ -1,5 +1,5 @@ use crate::{ - accounts::{render_accounts_route, AccountsAction}, + accounts::render_accounts_route, actionbar::NoteAction, app::{get_active_columns, get_active_columns_mut, get_decks_mut}, column::ColumnsAction, @@ -27,6 +27,8 @@ use crate::{ Damus, }; +use notedeck::{AccountsAction, AppContext}; + use egui_nav::{Nav, NavAction, NavResponse, NavUiType}; use nostrdb::{Ndb, Transaction}; use tracing::{error, info}; @@ -48,23 +50,23 @@ pub enum SwitchingAction { impl SwitchingAction { /// process the action, and return whether switching occured - pub fn process(&self, app: &mut Damus) -> bool { + pub fn process(&self, app: &mut Damus, ctx: &mut AppContext<'_>) -> bool { match &self { SwitchingAction::Accounts(account_action) => match *account_action { - AccountsAction::Switch(index) => app.accounts.select_account(index), - AccountsAction::Remove(index) => app.accounts.remove_account(index), + AccountsAction::Switch(index) => ctx.accounts.select_account(index), + AccountsAction::Remove(index) => ctx.accounts.remove_account(index), }, SwitchingAction::Columns(columns_action) => match *columns_action { ColumnsAction::Remove(index) => { - get_active_columns_mut(&app.accounts, &mut app.decks_cache).delete_column(index) + get_active_columns_mut(ctx.accounts, &mut app.decks_cache).delete_column(index) } }, SwitchingAction::Decks(decks_action) => match *decks_action { DecksAction::Switch(index) => { - get_decks_mut(&app.accounts, &mut app.decks_cache).set_active(index) + get_decks_mut(ctx.accounts, &mut app.decks_cache).set_active(index) } DecksAction::Removing(index) => { - get_decks_mut(&app.accounts, &mut app.decks_cache).remove_deck(index) + get_decks_mut(ctx.accounts, &mut app.decks_cache).remove_deck(index) } }, } @@ -98,7 +100,7 @@ impl RenderNavResponse { } #[must_use = "Make sure to save columns if result is true"] - pub fn process_render_nav_response(&self, app: &mut Damus) -> bool { + pub fn process_render_nav_response(&self, app: &mut Damus, ctx: &mut AppContext<'_>) -> bool { let mut switching_occured: bool = false; let col = self.column; @@ -111,46 +113,51 @@ impl RenderNavResponse { // start returning when we're finished posting match action { RenderNavAction::Back => { - app.columns_mut().column_mut(col).router_mut().go_back(); + app.columns_mut(ctx.accounts) + .column_mut(col) + .router_mut() + .go_back(); } RenderNavAction::RemoveColumn => { - let tl = app.columns().find_timeline_for_column_index(col); + let tl = app + .columns(ctx.accounts) + .find_timeline_for_column_index(col); if let Some(timeline) = tl { - unsubscribe_timeline(app.ndb(), timeline); + unsubscribe_timeline(ctx.ndb, timeline); } - app.columns_mut().delete_column(col); + app.columns_mut(ctx.accounts).delete_column(col); switching_occured = true; } RenderNavAction::PostAction(post_action) => { - let txn = Transaction::new(&app.ndb).expect("txn"); - let _ = post_action.execute(&app.ndb, &txn, &mut app.pool, &mut app.drafts); - get_active_columns_mut(&app.accounts, &mut app.decks_cache) + let txn = Transaction::new(ctx.ndb).expect("txn"); + let _ = post_action.execute(ctx.ndb, &txn, ctx.pool, &mut app.drafts); + get_active_columns_mut(ctx.accounts, &mut app.decks_cache) .column_mut(col) .router_mut() .go_back(); } RenderNavAction::NoteAction(note_action) => { - let txn = Transaction::new(&app.ndb).expect("txn"); + let txn = Transaction::new(ctx.ndb).expect("txn"); note_action.execute_and_process_result( - &app.ndb, - get_active_columns_mut(&app.accounts, &mut app.decks_cache), + ctx.ndb, + get_active_columns_mut(ctx.accounts, &mut app.decks_cache), col, &mut app.threads, &mut app.profiles, - &mut app.note_cache, - &mut app.pool, + ctx.note_cache, + ctx.pool, &txn, - &app.accounts.mutefun(), + &ctx.accounts.mutefun(), ); } RenderNavAction::SwitchingAction(switching_action) => { - switching_occured = switching_action.process(app); + switching_occured = switching_action.process(app, ctx); } } } @@ -158,37 +165,41 @@ impl RenderNavResponse { if let Some(action) = self.response.action { match action { NavAction::Returned => { - let r = app.columns_mut().column_mut(col).router_mut().pop(); - let txn = Transaction::new(&app.ndb).expect("txn"); + let r = app + .columns_mut(ctx.accounts) + .column_mut(col) + .router_mut() + .pop(); + let txn = Transaction::new(ctx.ndb).expect("txn"); if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r { let root_id = { - crate::note::root_note_id_from_selected_id( - &app.ndb, - &mut app.note_cache, + notedeck::note::root_note_id_from_selected_id( + ctx.ndb, + ctx.note_cache, &txn, id.bytes(), ) }; Thread::unsubscribe_locally( &txn, - &app.ndb, - &mut app.note_cache, + ctx.ndb, + ctx.note_cache, &mut app.threads, - &mut app.pool, + ctx.pool, root_id, - &app.accounts.mutefun(), + &ctx.accounts.mutefun(), ); } if let Some(Route::Timeline(TimelineRoute::Profile(pubkey))) = r { Profile::unsubscribe_locally( &txn, - &app.ndb, - &mut app.note_cache, + ctx.ndb, + ctx.note_cache, &mut app.profiles, - &mut app.pool, + ctx.pool, pubkey.bytes(), - &app.accounts.mutefun(), + &ctx.accounts.mutefun(), ); } @@ -196,7 +207,7 @@ impl RenderNavResponse { } NavAction::Navigated => { - let cur_router = app.columns_mut().column_mut(col).router_mut(); + let cur_router = app.columns_mut(ctx.accounts).column_mut(col).router_mut(); cur_router.navigating = false; if cur_router.is_replacing() { cur_router.remove_previous_routes(); @@ -218,20 +229,21 @@ impl RenderNavResponse { fn render_nav_body( ui: &mut egui::Ui, app: &mut Damus, + ctx: &mut AppContext<'_>, top: &Route, col: usize, ) -> Option { match top { Route::Timeline(tlr) => render_timeline_route( - &app.ndb, - get_active_columns_mut(&app.accounts, &mut app.decks_cache), + ctx.ndb, + get_active_columns_mut(ctx.accounts, &mut app.decks_cache), &mut app.drafts, - &mut app.img_cache, - &mut app.unknown_ids, - &mut app.note_cache, + ctx.img_cache, + ctx.unknown_ids, + ctx.note_cache, &mut app.threads, &mut app.profiles, - &mut app.accounts, + ctx.accounts, *tlr, col, app.textmode, @@ -240,36 +252,36 @@ fn render_nav_body( Route::Accounts(amr) => { let mut action = render_accounts_route( ui, - &app.ndb, + ctx.ndb, col, - &mut app.img_cache, - &mut app.accounts, + ctx.img_cache, + ctx.accounts, &mut app.decks_cache, &mut app.view_state.login, *amr, ); - let txn = Transaction::new(&app.ndb).expect("txn"); - action.process_action(&mut app.unknown_ids, &app.ndb, &txn); + let txn = Transaction::new(ctx.ndb).expect("txn"); + action.process_action(ctx.unknown_ids, ctx.ndb, &txn); action .accounts_action .map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f))) } Route::Relays => { - let manager = RelayPoolManager::new(app.pool_mut()); + let manager = RelayPoolManager::new(ctx.pool); RelayView::new(manager).ui(ui); None } Route::ComposeNote => { - let kp = app.accounts.get_selected_account()?.to_full()?; + let kp = ctx.accounts.get_selected_account()?.to_full()?; let draft = app.drafts.compose_mut(); - let txn = Transaction::new(&app.ndb).expect("txn"); + let txn = Transaction::new(ctx.ndb).expect("txn"); let post_response = ui::PostView::new( - &app.ndb, + ctx.ndb, draft, PostType::New, - &mut app.img_cache, - &mut app.note_cache, + ctx.img_cache, + ctx.note_cache, kp, ) .ui(&txn, ui); @@ -277,7 +289,7 @@ fn render_nav_body( post_response.action.map(Into::into) } Route::AddColumn(route) => { - render_add_column_routes(ui, app, col, route); + render_add_column_routes(ui, app, ctx, col, route); None } @@ -290,14 +302,14 @@ fn render_nav_body( let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default(); let mut resp = None; if let Some(config_resp) = ConfigureDeckView::new(new_deck_state).ui(ui) { - if let Some(cur_acc) = app.accounts.get_selected_account() { + if let Some(cur_acc) = ctx.accounts.get_selected_account() { app.decks_cache.add_deck( cur_acc.pubkey, Deck::new(config_resp.icon, config_resp.name), ); // set new deck as active - let cur_index = get_decks_mut(&app.accounts, &mut app.decks_cache) + let cur_index = get_decks_mut(ctx.accounts, &mut app.decks_cache) .decks() .len() - 1; @@ -307,7 +319,7 @@ fn render_nav_body( } new_deck_state.clear(); - get_active_columns_mut(&app.accounts, &mut app.decks_cache) + get_active_columns_mut(ctx.accounts, &mut app.decks_cache) .get_first_router() .go_back(); } @@ -315,13 +327,13 @@ fn render_nav_body( } Route::EditDeck(index) => { let mut action = None; - let cur_deck = get_decks_mut(&app.accounts, &mut app.decks_cache) + let cur_deck = get_decks_mut(ctx.accounts, &mut app.decks_cache) .decks_mut() .get_mut(*index) .expect("index wasn't valid"); let id = ui.id().with(( "edit-deck", - app.accounts.get_selected_account().map(|k| k.pubkey), + ctx.accounts.get_selected_account().map(|k| k.pubkey), index, )); let deck_state = app @@ -340,7 +352,7 @@ fn render_nav_body( ))); } } - get_active_columns_mut(&app.accounts, &mut app.decks_cache) + get_active_columns_mut(ctx.accounts, &mut app.decks_cache) .get_first_router() .go_back(); } @@ -351,25 +363,46 @@ fn render_nav_body( } #[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"] -pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) -> RenderNavResponse { - let col_id = get_active_columns(&app.accounts, &app.decks_cache).get_column_id_at_index(col); +pub fn render_nav( + col: usize, + app: &mut Damus, + ctx: &mut AppContext<'_>, + ui: &mut egui::Ui, +) -> RenderNavResponse { + let col_id = get_active_columns(ctx.accounts, &app.decks_cache).get_column_id_at_index(col); // TODO(jb55): clean up this router_mut mess by using Router in egui-nav directly - let nav_response = Nav::new(&app.columns().column(col).router().routes().clone()) - .navigating(app.columns_mut().column_mut(col).router_mut().navigating) - .returning(app.columns_mut().column_mut(col).router_mut().returning) - .id_source(egui::Id::new(col_id)) - .show_mut(ui, |ui, render_type, nav| match render_type { - NavUiType::Title => NavTitle::new( - &app.ndb, - &mut app.img_cache, - get_active_columns_mut(&app.accounts, &mut app.decks_cache), - app.accounts.get_selected_account().map(|a| &a.pubkey), - nav.routes(), - ) - .show(ui), - NavUiType::Body => render_nav_body(ui, app, nav.routes().last().expect("top"), col), - }); + let nav_response = Nav::new( + &app.columns(ctx.accounts) + .column(col) + .router() + .routes() + .clone(), + ) + .navigating( + app.columns_mut(ctx.accounts) + .column_mut(col) + .router_mut() + .navigating, + ) + .returning( + app.columns_mut(ctx.accounts) + .column_mut(col) + .router_mut() + .returning, + ) + .id_source(egui::Id::new(col_id)) + .show_mut(ui, |ui, render_type, nav| match render_type { + NavUiType::Title => NavTitle::new( + ctx.ndb, + ctx.img_cache, + get_active_columns_mut(ctx.accounts, &mut app.decks_cache), + ctx.accounts.get_selected_account().map(|a| &a.pubkey), + nav.routes(), + ) + .show(ui), + NavUiType::Body => render_nav_body(ui, app, ctx, nav.routes().last().expect("top"), col), + }); RenderNavResponse::new(col, nav_response) } diff --git a/crates/notedeck_columns/src/notes_holder.rs b/crates/notedeck_columns/src/notes_holder.rs index 314762f..7bd8ad1 100644 --- a/crates/notedeck_columns/src/notes_holder.rs +++ b/crates/notedeck_columns/src/notes_holder.rs @@ -2,11 +2,12 @@ use std::collections::HashMap; use enostr::{Filter, RelayPool}; use nostrdb::{Ndb, Transaction}; +use notedeck::{MuteFun, NoteCache, NoteRef, NoteRefsUnkIdAction}; use tracing::{debug, info, warn}; use crate::{ - actionbar::NotesHolderResult, multi_subscriber::MultiSubscriber, muted::MuteFun, note::NoteRef, - notecache::NoteCache, timeline::TimelineTab, unknowns::NoteRefsUnkIdAction, Error, Result, + actionbar::NotesHolderResult, multi_subscriber::MultiSubscriber, timeline::TimelineTab, Error, + Result, }; pub struct NotesHolderStorage { diff --git a/crates/notedeck_columns/src/profile.rs b/crates/notedeck_columns/src/profile.rs index 2ddcf97..6ab185f 100644 --- a/crates/notedeck_columns/src/profile.rs +++ b/crates/notedeck_columns/src/profile.rs @@ -1,12 +1,10 @@ use enostr::{Filter, Pubkey}; use nostrdb::{FilterBuilder, Ndb, ProfileRecord, Transaction}; +use notedeck::{filter::default_limit, FilterState, MuteFun, NoteCache, NoteRef}; + use crate::{ - filter::{self, FilterState}, multi_subscriber::MultiSubscriber, - muted::MuteFun, - note::NoteRef, - notecache::NoteCache, notes_holder::NotesHolder, timeline::{copy_notes_into_timeline, PubkeySource, Timeline, TimelineKind}, }; @@ -79,7 +77,7 @@ impl Profile { vec![Filter::new() .authors([pk]) .kinds([1]) - .limit(filter::default_limit())] + .limit(default_limit())] } } diff --git a/crates/notedeck_columns/src/storage/decks.rs b/crates/notedeck_columns/src/storage/decks.rs index cd4d066..6ce6ced 100644 --- a/crates/notedeck_columns/src/storage/decks.rs +++ b/crates/notedeck_columns/src/storage/decks.rs @@ -15,7 +15,7 @@ use crate::{ Error, }; -use super::{write_file, DataPath, DataPathType, Directory}; +use notedeck::{storage, DataPath, DataPathType, Directory}; pub static DECKS_CACHE_FILE: &str = "decks_cache.json"; @@ -51,7 +51,7 @@ pub fn save_decks_cache(path: &DataPath, decks_cache: &DecksCache) { let data_path = path.path(DataPathType::Setting); - if let Err(e) = write_file( + if let Err(e) = storage::write_file( &data_path, DECKS_CACHE_FILE.to_string(), &serialized_decks_cache, @@ -761,12 +761,13 @@ impl fmt::Display for Selection { #[cfg(test)] mod tests { - use enostr::Pubkey; + //use enostr::Pubkey; - use crate::{route::Route, test_data::test_app, timeline::TimelineRoute}; + //use crate::{route::Route, timeline::TimelineRoute}; - use super::deserialize_columns; + //use super::deserialize_columns; + /* TODO: re-enable once we have test_app working again #[test] fn test_deserialize_columns() { let serialized = vec![ @@ -800,4 +801,5 @@ mod tests { panic!("The second router route is not a TimelineRoute::Timeline variant"); } } + */ } diff --git a/crates/notedeck_columns/src/storage/migration.rs b/crates/notedeck_columns/src/storage/migration.rs index 733d6ad..65e41e5 100644 --- a/crates/notedeck_columns/src/storage/migration.rs +++ b/crates/notedeck_columns/src/storage/migration.rs @@ -9,10 +9,10 @@ use crate::{ route::Route, timeline::{kind::ListKind, PubkeySource, Timeline, TimelineId, TimelineKind, TimelineRoute}, ui::add_column::AddColumnRoute, - Error, + Result, }; -use super::{DataPath, DataPathType, Directory}; +use notedeck::{DataPath, DataPathType, Directory}; pub static COLUMNS_FILE: &str = "columns.json"; @@ -123,7 +123,7 @@ struct MigrationColumn { } impl<'de> Deserialize<'de> for MigrationColumn { - fn deserialize(deserializer: D) -> Result + fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de>, { @@ -266,9 +266,11 @@ pub fn deserialize_columns(path: &DataPath, ndb: &Ndb, user: Option<&[u8; 32]>) string_to_columns(columns_json(path)?, ndb, user) } -fn deserialize_columns_string(serialized_columns: String) -> Result { - serde_json::from_str::(&serialized_columns) - .map_err(|e| Error::Generic(e.to_string())) +fn deserialize_columns_string(serialized_columns: String) -> Result { + Ok( + serde_json::from_str::(&serialized_columns) + .map_err(notedeck::Error::Json)?, + ) } #[cfg(test)] diff --git a/crates/notedeck_columns/src/storage/mod.rs b/crates/notedeck_columns/src/storage/mod.rs index a64772d..cda44ee 100644 --- a/crates/notedeck_columns/src/storage/mod.rs +++ b/crates/notedeck_columns/src/storage/mod.rs @@ -1,15 +1,5 @@ mod decks; -mod file_key_storage; -mod file_storage; mod migration; pub use decks::{load_decks_cache, save_decks_cache, DECKS_CACHE_FILE}; -pub use file_key_storage::FileKeyStorage; -pub use file_storage::{delete_file, write_file, DataPath, DataPathType, Directory}; pub use migration::{deserialize_columns, COLUMNS_FILE}; - -#[cfg(target_os = "macos")] -mod security_framework_key_storage; - -pub mod key_storage_impl; -pub use key_storage_impl::{KeyStorageResponse, KeyStorageType}; diff --git a/crates/notedeck_columns/src/support.rs b/crates/notedeck_columns/src/support.rs index d0cab00..a705fb3 100644 --- a/crates/notedeck_columns/src/support.rs +++ b/crates/notedeck_columns/src/support.rs @@ -1,6 +1,6 @@ use tracing::error; -use crate::storage::{DataPath, DataPathType, Directory}; +use notedeck::{DataPath, DataPathType, Directory}; pub struct Support { directory: Directory, diff --git a/crates/notedeck_columns/src/test_data.rs b/crates/notedeck_columns/src/test_data.rs index 8e95cf9..4801224 100644 --- a/crates/notedeck_columns/src/test_data.rs +++ b/crates/notedeck_columns/src/test_data.rs @@ -1,9 +1,5 @@ -use std::path::Path; - -use enostr::{FullKeypair, Pubkey, RelayPool}; -use nostrdb::{ProfileRecord, Transaction}; - -use crate::{user_account::UserAccount, Damus}; +use enostr::RelayPool; +use nostrdb::ProfileRecord; #[allow(unused_must_use)] pub fn sample_pool() -> RelayPool { @@ -70,6 +66,7 @@ pub fn test_profile_record() -> ProfileRecord<'static> { ProfileRecord::new_owned(&TEST_PROFILE_DATA).unwrap() } +/* const TEN_ACCOUNT_HEXES: [&str; 10] = [ "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", @@ -95,7 +92,7 @@ pub fn get_test_accounts() -> Vec { } pub fn test_app() -> Damus { - let db_dir = Path::new("."); + let db_dir = Path::new("target/testdbs/test_app"); let path = db_dir.to_str().unwrap(); let mut app = Damus::mock(path); @@ -109,3 +106,4 @@ pub fn test_app() -> Damus { app } +*/ diff --git a/crates/notedeck_columns/src/thread.rs b/crates/notedeck_columns/src/thread.rs index fe57292..27864d2 100644 --- a/crates/notedeck_columns/src/thread.rs +++ b/crates/notedeck_columns/src/thread.rs @@ -1,12 +1,11 @@ use crate::{ multi_subscriber::MultiSubscriber, - muted::MuteFun, - note::NoteRef, - notecache::NoteCache, notes_holder::NotesHolder, timeline::{TimelineTab, ViewFilter}, }; + use nostrdb::{Filter, FilterBuilder, Ndb, Transaction}; +use notedeck::{MuteFun, NoteCache, NoteRef}; #[derive(Default)] pub struct Thread { diff --git a/crates/notedeck_columns/src/timeline/kind.rs b/crates/notedeck_columns/src/timeline/kind.rs index 94d2233..2bad59f 100644 --- a/crates/notedeck_columns/src/timeline/kind.rs +++ b/crates/notedeck_columns/src/timeline/kind.rs @@ -1,9 +1,8 @@ -use crate::error::{Error, FilterError}; -use crate::filter; -use crate::filter::FilterState; +use crate::error::Error; use crate::timeline::Timeline; use enostr::{Filter, Pubkey}; use nostrdb::{Ndb, Transaction}; +use notedeck::{filter::default_limit, FilterError, FilterState}; use serde::{Deserialize, Serialize}; use std::{borrow::Cow, fmt::Display}; use tracing::{error, warn}; @@ -111,7 +110,7 @@ impl TimelineKind { TimelineKind::Universe, FilterState::ready(vec![Filter::new() .kinds([1]) - .limit(filter::default_limit()) + .limit(default_limit()) .build()]), )), @@ -129,7 +128,7 @@ impl TimelineKind { let filter = Filter::new() .authors([pk]) .kinds([1]) - .limit(filter::default_limit()) + .limit(default_limit()) .build(); Some(Timeline::new( @@ -147,7 +146,7 @@ impl TimelineKind { let notifications_filter = Filter::new() .pubkeys([pk]) .kinds([1]) - .limit(crate::filter::default_limit()) + .limit(default_limit()) .build(); Some(Timeline::new( @@ -179,10 +178,12 @@ impl TimelineKind { } match Timeline::contact_list(&results[0].note, pk_src.clone()) { - Err(Error::Filter(FilterError::EmptyContactList)) => Some(Timeline::new( - TimelineKind::contact_list(pk_src), - FilterState::needs_remote(vec![contact_filter]), - )), + Err(Error::App(notedeck::Error::Filter(FilterError::EmptyContactList))) => { + Some(Timeline::new( + TimelineKind::contact_list(pk_src), + FilterState::needs_remote(vec![contact_filter]), + )) + } Err(e) => { error!("Unexpected error: {e}"); None diff --git a/crates/notedeck_columns/src/timeline/mod.rs b/crates/notedeck_columns/src/timeline/mod.rs index b11c3d2..23c27ae 100644 --- a/crates/notedeck_columns/src/timeline/mod.rs +++ b/crates/notedeck_columns/src/timeline/mod.rs @@ -1,16 +1,16 @@ use crate::{ column::Columns, decks::DecksCache, - error::{Error, FilterError}, - filter::{self, FilterState, FilterStates}, - muted::MuteFun, - note::NoteRef, - notecache::{CachedNote, NoteCache}, + error::Error, subscriptions::{self, SubKind, Subscriptions}, - unknowns::UnknownIds, Result, }; +use notedeck::{ + filter, CachedNote, FilterError, FilterState, FilterStates, MuteFun, NoteCache, NoteRef, + UnknownIds, +}; + use std::fmt; use std::sync::atomic::{AtomicU32, Ordering}; @@ -271,7 +271,9 @@ impl Timeline { let timeline = timelines .get_mut(timeline_idx) .ok_or(Error::TimelineNotFound)?; - let sub = timeline.subscription.ok_or(Error::no_active_sub())?; + let sub = timeline + .subscription + .ok_or(Error::App(notedeck::Error::no_active_sub()))?; let new_note_ids = ndb.poll_for_notes(sub, 500); if new_note_ids.is_empty() { @@ -535,7 +537,7 @@ fn setup_initial_timeline( "querying nostrdb sub {:?} {:?}", timeline.subscription, timeline.filter ); - let lim = filters[0].limit().unwrap_or(crate::filter::default_limit()) as i32; + let lim = filters[0].limit().unwrap_or(filter::default_limit()) as i32; let notes = ndb .query(&txn, filters, lim)? .into_iter() @@ -607,7 +609,7 @@ fn setup_timeline_nostrdb_sub( let filter_state = timeline .filter .get_any_ready() - .ok_or(Error::empty_contact_list())? + .ok_or(Error::App(notedeck::Error::empty_contact_list()))? .to_owned(); setup_initial_timeline(ndb, timeline, note_cache, &filter_state, is_muted)?; @@ -661,7 +663,7 @@ pub fn is_timeline_ready( // TODO: into_follow_filter is hardcoded to contact lists, let's generalize match filter { - Err(Error::Filter(e)) => { + Err(notedeck::Error::Filter(e)) => { error!("got broken when building filter {e}"); timeline .filter diff --git a/crates/notedeck_columns/src/timeline/route.rs b/crates/notedeck_columns/src/timeline/route.rs index 5ccca2d..5048589 100644 --- a/crates/notedeck_columns/src/timeline/route.rs +++ b/crates/notedeck_columns/src/timeline/route.rs @@ -1,11 +1,7 @@ use crate::{ - accounts::Accounts, column::Columns, draft::Drafts, - imgcache::ImageCache, - muted::MuteFun, nav::RenderNavAction, - notecache::NoteCache, notes_holder::NotesHolderStorage, profile::Profile, thread::Thread, @@ -15,11 +11,11 @@ use crate::{ note::{NoteOptions, QuoteRepostView}, profile::ProfileView, }, - unknowns::UnknownIds, }; use enostr::{NoteId, Pubkey}; use nostrdb::{Ndb, Transaction}; +use notedeck::{Accounts, ImageCache, MuteFun, NoteCache, UnknownIds}; #[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum TimelineRoute { diff --git a/crates/notedeck_columns/src/ui/account_login_view.rs b/crates/notedeck_columns/src/ui/account_login_view.rs index 012d0b3..d2321e1 100644 --- a/crates/notedeck_columns/src/ui/account_login_view.rs +++ b/crates/notedeck_columns/src/ui/account_login_view.rs @@ -1,10 +1,10 @@ -use crate::app_style::NotedeckTextStyle; use crate::key_parsing::AcquireKeyError; use crate::login_manager::AcquireKeyState; use crate::ui::{Preview, PreviewConfig, View}; use egui::TextEdit; use egui::{Align, Button, Color32, Frame, InnerResponse, Margin, RichText, Vec2}; use enostr::Keypair; +use notedeck::NotedeckTextStyle; pub struct AccountLoginView<'a> { manager: &'a mut AcquireKeyState, diff --git a/crates/notedeck_columns/src/ui/accounts.rs b/crates/notedeck_columns/src/ui/accounts.rs index 7e7bc0f..01ad659 100644 --- a/crates/notedeck_columns/src/ui/accounts.rs +++ b/crates/notedeck_columns/src/ui/accounts.rs @@ -1,14 +1,9 @@ use crate::colors::PINK; -use crate::imgcache::ImageCache; -use crate::{ - accounts::Accounts, - ui::{Preview, PreviewConfig, View}, - Damus, -}; use egui::{ Align, Button, Frame, Image, InnerResponse, Layout, RichText, ScrollArea, Ui, UiBuilder, Vec2, }; use nostrdb::{Ndb, Transaction}; +use notedeck::{Accounts, ImageCache}; use super::profile::preview::SimpleProfilePreview; @@ -180,7 +175,7 @@ fn scroll_area() -> ScrollArea { } fn add_account_button() -> Button<'static> { - let img_data = egui::include_image!("../../assets/icons/add_account_icon_4x.png"); + let img_data = egui::include_image!("../../../../assets/icons/add_account_icon_4x.png"); let img = Image::new(img_data).fit_to_exact_size(Vec2::new(48.0, 48.0)); Button::image_and_text( img, @@ -195,48 +190,3 @@ fn add_account_button() -> Button<'static> { fn sign_out_button() -> egui::Button<'static> { egui::Button::new(RichText::new("Sign out")) } - -// PREVIEWS -mod preview { - - use super::*; - use crate::{accounts::process_accounts_view_response, test_data}; - - pub struct AccountsPreview { - app: Damus, - } - - impl AccountsPreview { - fn new() -> Self { - let app = test_data::test_app(); - AccountsPreview { app } - } - } - - impl View for AccountsPreview { - fn ui(&mut self, ui: &mut egui::Ui) { - ui.add_space(24.0); - // TODO(jb55): maybe just use render_nav here so we can step through routes - if let Some(response) = - AccountsView::new(&self.app.ndb, &self.app.accounts, &mut self.app.img_cache) - .ui(ui) - .inner - { - process_accounts_view_response( - &mut self.app.accounts, - &mut self.app.decks_cache, - 0, - response, - ); - } - } - } - - impl Preview for AccountsView<'_> { - type Prev = AccountsPreview; - - fn preview(_cfg: PreviewConfig) -> Self::Prev { - AccountsPreview::new() - } - } -} diff --git a/crates/notedeck_columns/src/ui/add_column.rs b/crates/notedeck_columns/src/ui/add_column.rs index a636b47..1db51b6 100644 --- a/crates/notedeck_columns/src/ui/add_column.rs +++ b/crates/notedeck_columns/src/ui/add_column.rs @@ -9,14 +9,14 @@ use nostrdb::Ndb; use tracing::error; use crate::{ - app_style::{get_font_size, NotedeckTextStyle}, login_manager::AcquireKeyState, timeline::{PubkeySource, Timeline, TimelineKind}, ui::anim::ICON_EXPANSION_MULTIPLE, - user_account::UserAccount, Damus, }; +use notedeck::{AppContext, NotedeckTextStyle, UserAccount}; + use super::{anim::AnimationHelper, padding}; pub enum AddColumnResponse { @@ -180,8 +180,8 @@ impl<'a> AddColumnView<'a> { let max_width = ui.available_width(); let title_style = NotedeckTextStyle::Body; let desc_style = NotedeckTextStyle::Button; - let title_min_font_size = get_font_size(ui.ctx(), &title_style); - let desc_min_font_size = get_font_size(ui.ctx(), &desc_style); + let title_min_font_size = notedeck::fonts::get_font_size(ui.ctx(), &title_style); + let desc_min_font_size = notedeck::fonts::get_font_size(ui.ctx(), &desc_style); let max_height = { let max_wrap_width = @@ -279,7 +279,7 @@ impl<'a> AddColumnView<'a> { vec.push(ColumnOptionData { title: "Universe", description: "See the whole nostr universe", - icon: egui::include_image!("../../assets/icons/universe_icon_dark_4x.png"), + icon: egui::include_image!("../../../../assets/icons/universe_icon_dark_4x.png"), option: AddColumnOption::Universe, }); @@ -293,20 +293,20 @@ impl<'a> AddColumnView<'a> { vec.push(ColumnOptionData { title: "Home timeline", description: "See recommended notes first", - icon: egui::include_image!("../../assets/icons/home_icon_dark_4x.png"), + icon: egui::include_image!("../../../../assets/icons/home_icon_dark_4x.png"), option: AddColumnOption::Home(source.clone()), }); } vec.push(ColumnOptionData { title: "Notifications", description: "Stay up to date with notifications and mentions", - icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"), + icon: egui::include_image!("../../../../assets/icons/notifications_icon_dark_4x.png"), option: AddColumnOption::UndecidedNotification, }); vec.push(ColumnOptionData { title: "Hashtag", description: "Stay up to date with a certain hashtag", - icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"), + icon: egui::include_image!("../../../../assets/icons/notifications_icon_dark_4x.png"), option: AddColumnOption::UndecidedHashtag, }); @@ -326,7 +326,9 @@ impl<'a> AddColumnView<'a> { vec.push(ColumnOptionData { title: "Your Notifications", description: "Stay up to date with your notifications and mentions", - icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"), + icon: egui::include_image!( + "../../../../assets/icons/notifications_icon_dark_4x.png" + ), option: AddColumnOption::Notification(source), }); } @@ -334,7 +336,7 @@ impl<'a> AddColumnView<'a> { vec.push(ColumnOptionData { title: "Someone else's Notifications", description: "Stay up to date with someone else's notifications and mentions", - icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"), + icon: egui::include_image!("../../../../assets/icons/notifications_icon_dark_4x.png"), option: AddColumnOption::ExternalNotification, }); @@ -352,19 +354,20 @@ struct ColumnOptionData { pub fn render_add_column_routes( ui: &mut egui::Ui, app: &mut Damus, + ctx: &mut AppContext<'_>, col: usize, route: &AddColumnRoute, ) { let mut add_column_view = AddColumnView::new( &mut app.view_state.id_state_map, - &app.ndb, - app.accounts.get_selected_account(), + ctx.ndb, + ctx.accounts.get_selected_account(), ); let resp = match route { AddColumnRoute::Base => add_column_view.ui(ui), AddColumnRoute::UndecidedNotification => add_column_view.notifications_ui(ui), AddColumnRoute::ExternalNotification => add_column_view.external_notification_ui(ui), - AddColumnRoute::Hashtag => hashtag_ui(ui, &app.ndb, &mut app.view_state.id_string_map), + AddColumnRoute::Hashtag => hashtag_ui(ui, ctx.ndb, &mut app.view_state.id_string_map), }; if let Some(resp) = resp { @@ -372,27 +375,34 @@ pub fn render_add_column_routes( AddColumnResponse::Timeline(mut timeline) => { crate::timeline::setup_new_timeline( &mut timeline, - &app.ndb, + ctx.ndb, &mut app.subscriptions, - &mut app.pool, - &mut app.note_cache, + ctx.pool, + ctx.note_cache, app.since_optimize, - &app.accounts.mutefun(), + &ctx.accounts.mutefun(), ); - app.columns_mut().add_timeline_to_column(col, timeline); + app.columns_mut(ctx.accounts) + .add_timeline_to_column(col, timeline); } AddColumnResponse::UndecidedNotification => { - app.columns_mut().column_mut(col).router_mut().route_to( - crate::route::Route::AddColumn(AddColumnRoute::UndecidedNotification), - ); + app.columns_mut(ctx.accounts) + .column_mut(col) + .router_mut() + .route_to(crate::route::Route::AddColumn( + AddColumnRoute::UndecidedNotification, + )); } AddColumnResponse::ExternalNotification => { - app.columns_mut().column_mut(col).router_mut().route_to( - crate::route::Route::AddColumn(AddColumnRoute::ExternalNotification), - ); + app.columns_mut(ctx.accounts) + .column_mut(col) + .router_mut() + .route_to(crate::route::Route::AddColumn( + AddColumnRoute::ExternalNotification, + )); } AddColumnResponse::Hashtag => { - app.columns_mut() + app.columns_mut(ctx.accounts) .column_mut(col) .router_mut() .route_to(crate::route::Route::AddColumn(AddColumnRoute::Hashtag)); @@ -438,44 +448,3 @@ pub fn hashtag_ui( }) .inner } - -mod preview { - use crate::{ - test_data, - ui::{Preview, PreviewConfig, View}, - Damus, - }; - - use super::AddColumnView; - - pub struct AddColumnPreview { - app: Damus, - } - - impl AddColumnPreview { - fn new() -> Self { - let app = test_data::test_app(); - - AddColumnPreview { app } - } - } - - impl View for AddColumnPreview { - fn ui(&mut self, ui: &mut egui::Ui) { - AddColumnView::new( - &mut self.app.view_state.id_state_map, - &self.app.ndb, - self.app.accounts.get_selected_account(), - ) - .ui(ui); - } - } - - impl Preview for AddColumnView<'_> { - type Prev = AddColumnPreview; - - fn preview(_cfg: PreviewConfig) -> Self::Prev { - AddColumnPreview::new() - } - } -} diff --git a/crates/notedeck_columns/src/ui/column/header.rs b/crates/notedeck_columns/src/ui/column/header.rs index f621ee1..9ca66c9 100644 --- a/crates/notedeck_columns/src/ui/column/header.rs +++ b/crates/notedeck_columns/src/ui/column/header.rs @@ -1,7 +1,5 @@ use crate::{ - app_style::NotedeckTextStyle, column::Columns, - imgcache::ImageCache, nav::RenderNavAction, route::Route, timeline::{TimelineId, TimelineRoute}, @@ -14,6 +12,7 @@ use crate::{ use egui::{RichText, Stroke, UiBuilder}; use enostr::Pubkey; use nostrdb::{Ndb, Transaction}; +use notedeck::{ImageCache, NotedeckTextStyle}; pub struct NavTitle<'a> { ndb: &'a Ndb, @@ -124,9 +123,9 @@ impl<'a> NavTitle<'a> { let max_size = icon_width * ICON_EXPANSION_MULTIPLE; let img_data = if ui.visuals().dark_mode { - egui::include_image!("../../../assets/icons/column_delete_icon_4x.png") + egui::include_image!("../../../../../assets/icons/column_delete_icon_4x.png") } else { - egui::include_image!("../../../assets/icons/column_delete_icon_light_4x.png") + egui::include_image!("../../../../../assets/icons/column_delete_icon_light_4x.png") }; let img = egui::Image::new(img_data).max_width(img_size); diff --git a/crates/notedeck_columns/src/ui/configure_deck.rs b/crates/notedeck_columns/src/ui/configure_deck.rs index 4794fc1..38404c1 100644 --- a/crates/notedeck_columns/src/ui/configure_deck.rs +++ b/crates/notedeck_columns/src/ui/configure_deck.rs @@ -1,11 +1,6 @@ +use crate::{app_style::deck_icon_font_sized, colors::PINK, deck_state::DeckState}; use egui::{vec2, Button, Color32, Label, RichText, Stroke, Ui, Widget}; - -use crate::{ - app_style::{deck_icon_font_sized, get_font_size, NotedeckTextStyle}, - colors::PINK, - deck_state::DeckState, - fonts::NamedFontFamily, -}; +use notedeck::{NamedFontFamily, NotedeckTextStyle}; use super::{ anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, @@ -39,7 +34,7 @@ impl<'a> ConfigureDeckView<'a> { pub fn ui(&mut self, ui: &mut Ui) -> Option { let title_font = egui::FontId::new( - get_font_size(ui.ctx(), &NotedeckTextStyle::Heading4), + notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Heading4), egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()), ); padding(16.0, ui, |ui| { @@ -52,7 +47,10 @@ impl<'a> ConfigureDeckView<'a> { ui.add(Label::new( RichText::new("We recommend short names") .color(ui.visuals().noninteractive().fg_stroke.color) - .size(get_font_size(ui.ctx(), &NotedeckTextStyle::Small)), + .size(notedeck::fonts::get_font_size( + ui.ctx(), + &NotedeckTextStyle::Small, + )), )); ui.add_space(32.0); diff --git a/crates/notedeck_columns/src/ui/mention.rs b/crates/notedeck_columns/src/ui/mention.rs index f3bb2b2..7fc5f38 100644 --- a/crates/notedeck_columns/src/ui/mention.rs +++ b/crates/notedeck_columns/src/ui/mention.rs @@ -1,5 +1,6 @@ -use crate::{colors, imgcache::ImageCache, ui}; +use crate::ui; use nostrdb::{Ndb, Transaction}; +use notedeck::ImageCache; pub struct Mention<'a> { ndb: &'a Ndb, @@ -66,6 +67,8 @@ fn mention_ui( #[cfg(feature = "profiling")] puffin::profile_function!(); + let link_color = ui.visuals().hyperlink_color; + ui.horizontal(|ui| { let profile = ndb.get_profile_by_pubkey(txn, pk).ok(); @@ -77,7 +80,7 @@ fn mention_ui( }; let resp = ui.add( - egui::Label::new(egui::RichText::new(name).color(colors::PURPLE).size(size)) + egui::Label::new(egui::RichText::new(name).color(link_color).size(size)) .selectable(selectable), ); diff --git a/crates/notedeck_columns/src/ui/mod.rs b/crates/notedeck_columns/src/ui/mod.rs index 8091c4e..4252b36 100644 --- a/crates/notedeck_columns/src/ui/mod.rs +++ b/crates/notedeck_columns/src/ui/mod.rs @@ -59,28 +59,3 @@ pub fn hline(ui: &egui::Ui) { let stroke = ui.style().visuals.widgets.noninteractive.bg_stroke; ui.painter().hline(rect.x_range(), resize_y, stroke); } - -#[inline] -#[allow(unreachable_code)] -pub fn is_compiled_as_mobile() -> bool { - #[cfg(any(target_os = "android", target_os = "ios"))] - { - true - } - - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - false - } -} - -/// Determine if the screen is narrow. This is useful for detecting mobile -/// contexts, but with the nuance that we may also have a wide android tablet. -pub fn is_narrow(ctx: &egui::Context) -> bool { - let screen_size = ctx.input(|c| c.screen_rect().size()); - screen_size.x < 550.0 -} - -pub fn is_oled() -> bool { - is_compiled_as_mobile() -} diff --git a/crates/notedeck_columns/src/ui/note/contents.rs b/crates/notedeck_columns/src/ui/note/contents.rs index 5aa3dd5..0a2da6a 100644 --- a/crates/notedeck_columns/src/ui/note/contents.rs +++ b/crates/notedeck_columns/src/ui/note/contents.rs @@ -1,14 +1,14 @@ use crate::actionbar::NoteAction; use crate::images::ImageType; -use crate::imgcache::ImageCache; -use crate::notecache::NoteCache; +use crate::ui; use crate::ui::note::{NoteOptions, NoteResponse}; use crate::ui::ProfilePic; -use crate::{colors, ui}; use egui::{Color32, Hyperlink, Image, RichText}; use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction}; use tracing::warn; +use notedeck::{ImageCache, NoteCache}; + pub struct NoteContents<'a> { ndb: &'a Ndb, img_cache: &'a mut ImageCache, @@ -94,8 +94,8 @@ pub fn render_note_preview( return ui .horizontal(|ui| { ui.spacing_mut().item_spacing.x = 0.0; - ui.colored_label(colors::PURPLE, "@"); - ui.colored_label(colors::PURPLE, &id_str[4..16]); + ui.colored_label(link_color, "@"); + ui.colored_label(link_color, &id_str[4..16]); }) .response; */ @@ -145,6 +145,7 @@ fn render_note_contents( let mut images: Vec = vec![]; let mut inline_note: Option<(&[u8; 32], &str)> = None; let hide_media = options.has_hide_media(); + let link_color = ui.visuals().hyperlink_color; let response = ui.horizontal_wrapped(|ui| { let blocks = if let Ok(blocks) = ndb.get_blocks_by_key(txn, note_key) { @@ -177,14 +178,14 @@ fn render_note_contents( } _ => { - ui.colored_label(colors::PURPLE, format!("@{}", &block.as_str()[4..16])); + ui.colored_label(link_color, format!("@{}", &block.as_str()[4..16])); } }, BlockType::Hashtag => { #[cfg(feature = "profiling")] puffin::profile_scope!("hashtag contents"); - ui.colored_label(colors::PURPLE, format!("#{}", block.as_str())); + ui.colored_label(link_color, format!("#{}", block.as_str())); } BlockType::Url => { @@ -195,7 +196,7 @@ fn render_note_contents( #[cfg(feature = "profiling")] puffin::profile_scope!("url contents"); ui.add(Hyperlink::from_label_and_url( - RichText::new(block.as_str()).color(colors::PURPLE), + RichText::new(block.as_str()).color(link_color), block.as_str(), )); } @@ -208,7 +209,7 @@ fn render_note_contents( } _ => { - ui.colored_label(colors::PURPLE, block.as_str()); + ui.colored_label(link_color, block.as_str()); } } } diff --git a/crates/notedeck_columns/src/ui/note/context.rs b/crates/notedeck_columns/src/ui/note/context.rs index 5b2283b..b0525e1 100644 --- a/crates/notedeck_columns/src/ui/note/context.rs +++ b/crates/notedeck_columns/src/ui/note/context.rs @@ -1,4 +1,3 @@ -use crate::colors; use egui::{Rect, Vec2}; use enostr::{NoteId, Pubkey}; use nostrdb::{Note, NoteKey}; @@ -136,7 +135,7 @@ impl NoteContextButton { let translated_radius = (cur_radius - 1.0) / 2.0; // This works in both themes - let color = colors::GRAY_SECONDARY; + let color = ui.style().visuals.noninteractive().fg_stroke.color; // Draw circles let painter = ui.painter_at(put_at); diff --git a/crates/notedeck_columns/src/ui/note/mod.rs b/crates/notedeck_columns/src/ui/note/mod.rs index 4aebb43..7460e5c 100644 --- a/crates/notedeck_columns/src/ui/note/mod.rs +++ b/crates/notedeck_columns/src/ui/note/mod.rs @@ -14,16 +14,14 @@ pub use reply::PostReplyView; use crate::{ actionbar::NoteAction, - app_style::NotedeckTextStyle, - colors, - imgcache::ImageCache, - notecache::{CachedNote, NoteCache}, ui::{self, View}, }; + use egui::emath::{pos2, Vec2}; use egui::{Id, Label, Pos2, Rect, Response, RichText, Sense}; use enostr::{NoteId, Pubkey}; use nostrdb::{Ndb, Note, NoteKey, NoteReply, Transaction}; +use notedeck::{CachedNote, ImageCache, NoteCache, NotedeckTextStyle}; use super::profile::preview::{get_display_name, one_line_display_name_widget}; @@ -80,15 +78,9 @@ fn reply_desc( let size = 10.0; let selectable = false; + let color = ui.style().visuals.noninteractive().fg_stroke.color; - ui.add( - Label::new( - RichText::new("replying to") - .size(size) - .color(colors::GRAY_SECONDARY), - ) - .selectable(selectable), - ); + ui.add(Label::new(RichText::new("replying to").size(size).color(color)).selectable(selectable)); let reply = if let Some(reply) = note_reply.reply() { reply @@ -99,14 +91,7 @@ fn reply_desc( let reply_note = if let Ok(reply_note) = ndb.get_note_by_id(txn, reply.id) { reply_note } else { - ui.add( - Label::new( - RichText::new("a note") - .size(size) - .color(colors::GRAY_SECONDARY), - ) - .selectable(selectable), - ); + ui.add(Label::new(RichText::new("a note").size(size).color(color)).selectable(selectable)); return; }; @@ -117,14 +102,7 @@ fn reply_desc( .size(size) .selectable(selectable), ); - ui.add( - Label::new( - RichText::new("'s note") - .size(size) - .color(colors::GRAY_SECONDARY), - ) - .selectable(selectable), - ); + ui.add(Label::new(RichText::new("'s note").size(size).color(color)).selectable(selectable)); } else if let Some(root) = note_reply.root() { // replying to another post in a thread, not the root @@ -137,12 +115,8 @@ fn reply_desc( .selectable(selectable), ); ui.add( - Label::new( - RichText::new("'s note") - .size(size) - .color(colors::GRAY_SECONDARY), - ) - .selectable(selectable), + Label::new(RichText::new("'s note").size(size).color(color)) + .selectable(selectable), ); } else { // replying to bob in alice's thread @@ -153,8 +127,7 @@ fn reply_desc( .selectable(selectable), ); ui.add( - Label::new(RichText::new("in").size(size).color(colors::GRAY_SECONDARY)) - .selectable(selectable), + Label::new(RichText::new("in").size(size).color(color)).selectable(selectable), ); ui.add( ui::Mention::new(ndb, img_cache, txn, root_note.pubkey()) @@ -162,12 +135,8 @@ fn reply_desc( .selectable(selectable), ); ui.add( - Label::new( - RichText::new("'s thread") - .size(size) - .color(colors::GRAY_SECONDARY), - ) - .selectable(selectable), + Label::new(RichText::new("'s thread").size(size).color(color)) + .selectable(selectable), ); } } else { @@ -177,12 +146,8 @@ fn reply_desc( .selectable(selectable), ); ui.add( - Label::new( - RichText::new("in someone's thread") - .size(size) - .color(colors::GRAY_SECONDARY), - ) - .selectable(selectable), + Label::new(RichText::new("in someone's thread").size(size).color(color)) + .selectable(selectable), ); } } @@ -382,6 +347,7 @@ impl<'a> NoteView<'a> { }); ui.add_space(6.0); let resp = ui.add(one_line_display_name_widget( + ui.visuals(), get_display_name(profile.as_ref().ok()), style, )); @@ -391,10 +357,11 @@ impl<'a> NoteView<'a> { ui.add(ui::ProfilePreview::new(rec, self.img_cache)); }); } + let color = ui.style().visuals.noninteractive().fg_stroke.color; ui.add_space(4.0); ui.label( RichText::new("Reposted") - .color(colors::GRAY_SECONDARY) + .color(color) .text_style(style.text_style()), ); }); @@ -690,9 +657,8 @@ fn render_note_actionbar( } fn secondary_label(ui: &mut egui::Ui, s: impl Into) { - ui.add(Label::new( - RichText::new(s).size(10.0).color(colors::GRAY_SECONDARY), - )); + let color = ui.style().visuals.noninteractive().fg_stroke.color; + ui.add(Label::new(RichText::new(s).size(10.0).color(color))); } fn render_reltime( @@ -718,9 +684,9 @@ fn render_reltime( fn reply_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { let img_data = if ui.style().visuals.dark_mode { - egui::include_image!("../../../assets/icons/reply.png") + egui::include_image!("../../../../../assets/icons/reply.png") } else { - egui::include_image!("../../../assets/icons/reply-dark.png") + egui::include_image!("../../../../../assets/icons/reply-dark.png") }; let (rect, size, resp) = @@ -737,9 +703,9 @@ fn reply_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { fn repost_icon(dark_mode: bool) -> egui::Image<'static> { let img_data = if dark_mode { - egui::include_image!("../../../assets/icons/repost_icon_4x.png") + egui::include_image!("../../../../../assets/icons/repost_icon_4x.png") } else { - egui::include_image!("../../../assets/icons/repost_light_4x.png") + egui::include_image!("../../../../../assets/icons/repost_light_4x.png") }; egui::Image::new(img_data) } diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs index 1c15baf..1b23b28 100644 --- a/crates/notedeck_columns/src/ui/note/post.rs +++ b/crates/notedeck_columns/src/ui/note/post.rs @@ -1,6 +1,4 @@ use crate::draft::{Draft, Drafts}; -use crate::imgcache::ImageCache; -use crate::notecache::NoteCache; use crate::post::NewPost; use crate::ui::{self, Preview, PreviewConfig, View}; use crate::Result; @@ -10,6 +8,8 @@ use enostr::{FilledKeypair, FullKeypair, NoteId, RelayPool}; use nostrdb::{Config, Ndb, Transaction}; use tracing::info; +use notedeck::{ImageCache, NoteCache}; + use super::contents::render_note_preview; pub struct PostView<'a> { diff --git a/crates/notedeck_columns/src/ui/note/quote_repost.rs b/crates/notedeck_columns/src/ui/note/quote_repost.rs index d88cdd6..1bb671d 100644 --- a/crates/notedeck_columns/src/ui/note/quote_repost.rs +++ b/crates/notedeck_columns/src/ui/note/quote_repost.rs @@ -1,7 +1,8 @@ use enostr::{FilledKeypair, NoteId}; use nostrdb::Ndb; +use notedeck::{ImageCache, NoteCache}; -use crate::{draft::Draft, imgcache::ImageCache, notecache::NoteCache, ui}; +use crate::{draft::Draft, ui}; use super::{PostResponse, PostType}; diff --git a/crates/notedeck_columns/src/ui/note/reply.rs b/crates/notedeck_columns/src/ui/note/reply.rs index 0abead1..4860028 100644 --- a/crates/notedeck_columns/src/ui/note/reply.rs +++ b/crates/notedeck_columns/src/ui/note/reply.rs @@ -1,11 +1,11 @@ use crate::draft::Draft; -use crate::imgcache::ImageCache; -use crate::notecache::NoteCache; use crate::ui; use crate::ui::note::{PostResponse, PostType}; use enostr::{FilledKeypair, NoteId}; use nostrdb::Ndb; +use notedeck::{ImageCache, NoteCache}; + pub struct PostReplyView<'a> { ndb: &'a Ndb, poster: FilledKeypair<'a>, diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs index 7271dba..da2b425 100644 --- a/crates/notedeck_columns/src/ui/profile/mod.rs +++ b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -8,12 +8,10 @@ use nostrdb::{Ndb, Transaction}; pub use picture::ProfilePic; pub use preview::ProfilePreview; -use crate::{ - actionbar::NoteAction, imgcache::ImageCache, muted::MuteFun, notecache::NoteCache, - notes_holder::NotesHolderStorage, profile::Profile, -}; +use crate::{actionbar::NoteAction, notes_holder::NotesHolderStorage, profile::Profile}; use super::timeline::{tabs_ui, TimelineTabView}; +use notedeck::{ImageCache, MuteFun, NoteCache}; pub struct ProfileView<'a> { pubkey: &'a Pubkey, diff --git a/crates/notedeck_columns/src/ui/profile/picture.rs b/crates/notedeck_columns/src/ui/profile/picture.rs index f9b2cc6..675ba8e 100644 --- a/crates/notedeck_columns/src/ui/profile/picture.rs +++ b/crates/notedeck_columns/src/ui/profile/picture.rs @@ -1,9 +1,10 @@ use crate::images::ImageType; -use crate::imgcache::ImageCache; use crate::ui::{Preview, PreviewConfig, View}; use egui::{vec2, Sense, TextureHandle}; use nostrdb::{Ndb, Transaction}; +use notedeck::ImageCache; + pub struct ProfilePic<'cache, 'url> { cache: &'cache mut ImageCache, url: &'url str, diff --git a/crates/notedeck_columns/src/ui/profile/preview.rs b/crates/notedeck_columns/src/ui/profile/preview.rs index e0a691e..3260fcd 100644 --- a/crates/notedeck_columns/src/ui/profile/preview.rs +++ b/crates/notedeck_columns/src/ui/profile/preview.rs @@ -1,8 +1,4 @@ -use crate::app_style::{get_font_size, NotedeckTextStyle}; -use crate::imgcache::ImageCache; -use crate::storage::{DataPath, DataPathType}; use crate::ui::ProfilePic; -use crate::user_account::UserAccount; use crate::{colors, images, DisplayName}; use egui::load::TexturePoll; use egui::{Frame, Label, RichText, Sense, Widget}; @@ -10,6 +6,8 @@ use egui_extras::Size; use enostr::{NoteId, Pubkey}; use nostrdb::{Ndb, ProfileRecord, Transaction}; +use notedeck::{DataPath, DataPathType, ImageCache, NotedeckTextStyle, UserAccount}; + pub struct ProfilePreview<'a, 'cache> { profile: &'a ProfileRecord<'a>, cache: &'cache mut ImageCache, @@ -121,7 +119,10 @@ impl egui::Widget for SimpleProfilePreview<'_, '_> { ui.add( Label::new( RichText::new("Read only") - .size(get_font_size(ui.ctx(), &NotedeckTextStyle::Tiny)) + .size(notedeck::fonts::get_font_size( + ui.ctx(), + &NotedeckTextStyle::Tiny, + )) .color(ui.visuals().warn_fg_color), ) .selectable(false), @@ -256,17 +257,16 @@ fn display_name_widget( } } -pub fn one_line_display_name_widget( - display_name: DisplayName<'_>, +pub fn one_line_display_name_widget<'a>( + visuals: &egui::Visuals, + display_name: DisplayName<'a>, style: NotedeckTextStyle, -) -> impl egui::Widget + '_ { +) -> impl egui::Widget + 'a { let text_style = style.text_style(); + let color = visuals.noninteractive().fg_stroke.color; + move |ui: &mut egui::Ui| match display_name { - DisplayName::One(n) => ui.label( - RichText::new(n) - .text_style(text_style) - .color(colors::GRAY_SECONDARY), - ), + DisplayName::One(n) => ui.label(RichText::new(n).text_style(text_style).color(color)), DisplayName::Both { display_name, @@ -274,7 +274,7 @@ pub fn one_line_display_name_widget( } => ui.label( RichText::new(display_name) .text_style(text_style) - .color(colors::GRAY_SECONDARY), + .color(color), ), } } diff --git a/crates/notedeck_columns/src/ui/relay.rs b/crates/notedeck_columns/src/ui/relay.rs index d448fd3..1c31414 100644 --- a/crates/notedeck_columns/src/ui/relay.rs +++ b/crates/notedeck_columns/src/ui/relay.rs @@ -2,8 +2,8 @@ use crate::relay_pool_manager::{RelayPoolManager, RelayStatus}; use crate::ui::{Preview, PreviewConfig, View}; use egui::{Align, Button, Frame, Layout, Margin, Rgba, RichText, Rounding, Ui, Vec2}; -use crate::app_style::NotedeckTextStyle; use enostr::RelayPool; +use notedeck::NotedeckTextStyle; pub struct RelayView<'a> { manager: RelayPoolManager<'a>, @@ -126,7 +126,7 @@ fn delete_button(_dark_mode: bool) -> egui::Button<'static> { egui::include_image!("../../assets/icons/delete_icon_4x.png") }; */ - let img_data = egui::include_image!("../../assets/icons/delete_icon_4x.png"); + let img_data = egui::include_image!("../../../../assets/icons/delete_icon_4x.png"); egui::Button::image(egui::Image::new(img_data).max_width(10.0)).frame(false) } @@ -165,12 +165,14 @@ fn show_connection_status(ui: &mut Ui, status: &RelayStatus) { fn get_connection_icon(status: &RelayStatus) -> egui::Image<'static> { let img_data = match status { - RelayStatus::Connected => egui::include_image!("../../assets/icons/connected_icon_4x.png"), + RelayStatus::Connected => { + egui::include_image!("../../../../assets/icons/connected_icon_4x.png") + } RelayStatus::Connecting => { - egui::include_image!("../../assets/icons/connecting_icon_4x.png") + egui::include_image!("../../../../assets/icons/connecting_icon_4x.png") } RelayStatus::Disconnected => { - egui::include_image!("../../assets/icons/disconnected_icon_4x.png") + egui::include_image!("../../../../assets/icons/disconnected_icon_4x.png") } }; diff --git a/crates/notedeck_columns/src/ui/side_panel.rs b/crates/notedeck_columns/src/ui/side_panel.rs index 6965f6a..71d4c2c 100644 --- a/crates/notedeck_columns/src/ui/side_panel.rs +++ b/crates/notedeck_columns/src/ui/side_panel.rs @@ -5,21 +5,18 @@ use egui::{ use tracing::{error, info}; use crate::{ - accounts::{Accounts, AccountsRoute}, + accounts::AccountsRoute, app::{get_active_columns_mut, get_decks_mut}, app_style::DECK_ICON_SIZE, colors, - column::Column, decks::{DecksAction, DecksCache}, - imgcache::ImageCache, nav::SwitchingAction, route::Route, support::Support, - theme_handler::ThemeHandler, - user_account::UserAccount, - Damus, }; +use notedeck::{Accounts, ImageCache, NotedeckTextStyle, ThemeHandler, UserAccount}; + use super::{ anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, configure_deck::deck_icon, @@ -386,9 +383,9 @@ fn settings_button(dark_mode: bool) -> impl Widget { let img_size = 24.0; let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget let img_data = if dark_mode { - egui::include_image!("../../assets/icons/settings_dark_4x.png") + egui::include_image!("../../../../assets/icons/settings_dark_4x.png") } else { - egui::include_image!("../../assets/icons/settings_light_4x.png") + egui::include_image!("../../../../assets/icons/settings_light_4x.png") }; let img = egui::Image::new(img_data).max_width(img_size); @@ -412,9 +409,9 @@ fn add_column_button(dark_mode: bool) -> impl Widget { let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget let img_data = if dark_mode { - egui::include_image!("../../assets/icons/add_column_dark_4x.png") + egui::include_image!("../../../../assets/icons/add_column_dark_4x.png") } else { - egui::include_image!("../../assets/icons/add_column_light_4x.png") + egui::include_image!("../../../../assets/icons/add_column_light_4x.png") }; let img = egui::Image::new(img_data).max_width(img_size); @@ -531,7 +528,7 @@ fn search_button() -> impl Widget { fn expand_side_panel_button() -> impl Widget { |ui: &mut egui::Ui| -> egui::Response { let img_size = 40.0; - let img_data = egui::include_image!("../../assets/damus_rounded_80.png"); + let img_data = egui::include_image!("../../../../assets/damus_rounded_80.png"); let img = egui::Image::new(img_data).max_width(img_size); ui.add(img) @@ -544,9 +541,9 @@ fn support_button() -> impl Widget { let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget let img_data = if ui.visuals().dark_mode { - egui::include_image!("../../assets/icons/help_icon_dark_4x.png") + egui::include_image!("../../../../assets/icons/help_icon_dark_4x.png") } else { - egui::include_image!("../../assets/icons/help_icon_inverted_4x.png") + egui::include_image!("../../../../assets/icons/help_icon_inverted_4x.png") }; let img = egui::Image::new(img_data).max_width(img_size); @@ -569,7 +566,7 @@ fn add_deck_button() -> impl Widget { let img_size = 40.0; let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget - let img_data = egui::include_image!("../../assets/icons/new_deck_icon_4x_dark.png"); + let img_data = egui::include_image!("../../../../assets/icons/new_deck_icon_4x_dark.png"); let img = egui::Image::new(img_data).max_width(img_size); let helper = AnimationHelper::new(ui, "new-deck-icon", vec2(max_size, max_size)); @@ -626,80 +623,18 @@ fn milestone_name() -> impl Widget { |ui: &mut egui::Ui| -> egui::Response { ui.vertical_centered(|ui| { let font = egui::FontId::new( - crate::app_style::get_font_size( + notedeck::fonts::get_font_size( ui.ctx(), - &crate::app_style::NotedeckTextStyle::Tiny, + &NotedeckTextStyle::Tiny, ), - egui::FontFamily::Name(crate::fonts::NamedFontFamily::Bold.as_str().into()), + egui::FontFamily::Name(notedeck::fonts::NamedFontFamily::Bold.as_str().into()), ); ui.add(Label::new( RichText::new("ALPHA") - .color(crate::colors::GRAY_SECONDARY) + .color( ui.style().visuals.noninteractive().fg_stroke.color) .font(font), ).selectable(false)).on_hover_text("Notedeck is an alpha product. Expect bugs and contact us when you run into issues.").on_hover_cursor(egui::CursorIcon::Help) }) .inner } } - -mod preview { - - use egui_extras::{Size, StripBuilder}; - - use crate::{ - app::get_active_columns_mut, - test_data, - ui::{Preview, PreviewConfig}, - }; - - use super::*; - - pub struct DesktopSidePanelPreview { - app: Damus, - } - - impl DesktopSidePanelPreview { - fn new() -> Self { - let mut app = test_data::test_app(); - get_active_columns_mut(&app.accounts, &mut app.decks_cache) - .add_column(Column::new(vec![Route::accounts()])); - DesktopSidePanelPreview { app } - } - } - - impl View for DesktopSidePanelPreview { - fn ui(&mut self, ui: &mut egui::Ui) { - StripBuilder::new(ui) - .size(Size::exact(SIDE_PANEL_WIDTH)) - .sizes(Size::remainder(), 0) - .clip(true) - .horizontal(|mut strip| { - strip.cell(|ui| { - let mut panel = DesktopSidePanel::new( - &self.app.ndb, - &mut self.app.img_cache, - self.app.accounts.get_selected_account(), - &self.app.decks_cache, - ); - let response = panel.show(ui); - - DesktopSidePanel::perform_action( - &mut self.app.decks_cache, - &self.app.accounts, - &mut self.app.support, - &mut self.app.theme, - response.action, - ); - }); - }); - } - } - - impl Preview for DesktopSidePanel<'_> { - type Prev = DesktopSidePanelPreview; - - fn preview(_cfg: PreviewConfig) -> Self::Prev { - DesktopSidePanelPreview::new() - } - } -} diff --git a/crates/notedeck_columns/src/ui/support.rs b/crates/notedeck_columns/src/ui/support.rs index 0dd6bab..42f03b5 100644 --- a/crates/notedeck_columns/src/ui/support.rs +++ b/crates/notedeck_columns/src/ui/support.rs @@ -1,14 +1,10 @@ use egui::{vec2, Button, Label, Layout, RichText}; use tracing::error; -use crate::{ - app_style::{get_font_size, NotedeckTextStyle}, - colors::PINK, - fonts::NamedFontFamily, - support::Support, -}; +use crate::{colors::PINK, support::Support}; use super::padding; +use notedeck::{NamedFontFamily, NotedeckTextStyle}; pub struct SupportView<'a> { support: &'a mut Support, @@ -23,7 +19,7 @@ impl<'a> SupportView<'a> { padding(8.0, ui, |ui| { ui.spacing_mut().item_spacing = egui::vec2(0.0, 8.0); let font = egui::FontId::new( - get_font_size(ui.ctx(), &NotedeckTextStyle::Body), + notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body), egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()), ); ui.add(Label::new(RichText::new("Running into a bug?").font(font))); @@ -32,7 +28,8 @@ impl<'a> SupportView<'a> { ui.label("Open your default email client to get help from the Damus team"); let size = vec2(120.0, 40.0); ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| { - let font_size = get_font_size(ui.ctx(), &NotedeckTextStyle::Body); + let font_size = + notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body); let button_resp = ui.add(open_email_button(font_size, size)); if button_resp.clicked() { if let Err(e) = open::that(self.support.get_mailto_url()) { @@ -54,9 +51,9 @@ impl<'a> SupportView<'a> { RichText::new("Step 2").text_style(NotedeckTextStyle::Heading3.text_style()), ); let size = vec2(80.0, 40.0); - let copy_button = Button::new( - RichText::new("Copy").size(get_font_size(ui.ctx(), &NotedeckTextStyle::Body)), - ) + let copy_button = Button::new(RichText::new("Copy").size( + notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body), + )) .fill(PINK) .min_size(size); padding(8.0, ui, |ui| { diff --git a/crates/notedeck_columns/src/ui/thread.rs b/crates/notedeck_columns/src/ui/thread.rs index 63b5d7d..c59e5b7 100644 --- a/crates/notedeck_columns/src/ui/thread.rs +++ b/crates/notedeck_columns/src/ui/thread.rs @@ -1,14 +1,12 @@ use crate::{ actionbar::NoteAction, - imgcache::ImageCache, - muted::MuteFun, - notecache::NoteCache, notes_holder::{NotesHolder, NotesHolderStorage}, thread::Thread, ui::note::NoteOptions, - unknowns::UnknownIds, }; + use nostrdb::{Ndb, NoteKey, Transaction}; +use notedeck::{ImageCache, MuteFun, NoteCache, UnknownIds}; use tracing::error; use super::timeline::TimelineTabView; diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs index 8fdeb01..3e6554a 100644 --- a/crates/notedeck_columns/src/ui/timeline.rs +++ b/crates/notedeck_columns/src/ui/timeline.rs @@ -1,13 +1,11 @@ use crate::actionbar::NoteAction; use crate::timeline::TimelineTab; -use crate::{ - column::Columns, imgcache::ImageCache, notecache::NoteCache, timeline::TimelineId, ui, - ui::note::NoteOptions, -}; +use crate::{column::Columns, timeline::TimelineId, ui, ui::note::NoteOptions}; use egui::containers::scroll_area::ScrollBarVisibility; use egui::{Direction, Layout}; use egui_tabs::TabColor; use nostrdb::{Ndb, Transaction}; +use notedeck::{ImageCache, NoteCache}; use tracing::{error, warn}; pub struct TimelineView<'a> { diff --git a/crates/notedeck_columns/src/ui/username.rs b/crates/notedeck_columns/src/ui/username.rs index 325f336..2444a67 100644 --- a/crates/notedeck_columns/src/ui/username.rs +++ b/crates/notedeck_columns/src/ui/username.rs @@ -1,6 +1,6 @@ -use crate::fonts::NamedFontFamily; use egui::{Color32, RichText, Widget}; use nostrdb::ProfileRecord; +use notedeck::fonts::NamedFontFamily; pub struct Username<'a> { profile: Option<&'a ProfileRecord<'a>>, diff --git a/crates/notedeck_columns/src/unknowns.rs b/crates/notedeck_columns/src/unknowns.rs index d034b95..13dcc02 100644 --- a/crates/notedeck_columns/src/unknowns.rs +++ b/crates/notedeck_columns/src/unknowns.rs @@ -1,356 +1,30 @@ -use crate::{ - column::Columns, - note::NoteRef, - notecache::{CachedNote, NoteCache}, - timeline::ViewFilter, - Result, -}; - -use enostr::{Filter, NoteId, Pubkey}; -use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction}; -use std::collections::HashSet; -use std::time::{Duration, Instant}; +use crate::{column::Columns, timeline::ViewFilter, Result}; +use nostrdb::{Ndb, NoteKey, Transaction}; +use notedeck::{CachedNote, NoteCache, UnknownIds}; use tracing::error; -#[must_use = "process_action should be used on this result"] -pub enum SingleUnkIdAction { - NoAction, - NeedsProcess(UnknownId), -} - -#[must_use = "process_action should be used on this result"] -pub enum NoteRefsUnkIdAction { - NoAction, - NeedsProcess(Vec), -} - -impl NoteRefsUnkIdAction { - pub fn new(refs: Vec) -> Self { - NoteRefsUnkIdAction::NeedsProcess(refs) - } - - pub fn no_action() -> Self { - Self::NoAction - } - - pub fn process_action( - &self, - txn: &Transaction, - ndb: &Ndb, - unk_ids: &mut UnknownIds, - note_cache: &mut NoteCache, - ) { - match self { - Self::NoAction => {} - Self::NeedsProcess(refs) => { - UnknownIds::update_from_note_refs(txn, ndb, unk_ids, note_cache, refs); - } - } - } -} - -impl SingleUnkIdAction { - pub fn new(id: UnknownId) -> Self { - SingleUnkIdAction::NeedsProcess(id) - } - - pub fn no_action() -> Self { - Self::NoAction - } - - pub fn pubkey(pubkey: Pubkey) -> Self { - SingleUnkIdAction::new(UnknownId::Pubkey(pubkey)) - } - - pub fn note_id(note_id: NoteId) -> Self { - SingleUnkIdAction::new(UnknownId::Id(note_id)) - } - - /// Some functions may return unknown id actions that need to be processed. - /// For example, when we add a new account we need to make sure we have the - /// profile for that account. This function ensures we add this to the - /// unknown id tracker without adding side effects to functions. - pub fn process_action(&self, ids: &mut UnknownIds, ndb: &Ndb, txn: &Transaction) { - match self { - Self::NeedsProcess(id) => { - ids.add_unknown_id_if_missing(ndb, txn, id); - } - Self::NoAction => {} - } - } -} - -/// Unknown Id searcher -#[derive(Default)] -pub struct UnknownIds { - ids: HashSet, - first_updated: Option, - last_updated: Option, -} - -impl UnknownIds { - /// Simple debouncer - pub fn ready_to_send(&self) -> bool { - if self.ids.is_empty() { - return false; - } - - // we trigger on first set - if self.first_updated == self.last_updated { - return true; - } - - let last_updated = if let Some(last) = self.last_updated { - last - } else { - // if we've - return true; - }; - - Instant::now() - last_updated >= Duration::from_secs(2) - } - - pub fn ids(&self) -> &HashSet { - &self.ids - } - - pub fn ids_mut(&mut self) -> &mut HashSet { - &mut self.ids - } - - pub fn clear(&mut self) { - self.ids = HashSet::default(); - } - - pub fn filter(&self) -> Option> { - let ids: Vec<&UnknownId> = self.ids.iter().collect(); - get_unknown_ids_filter(&ids) - } - - /// We've updated some unknown ids, update the last_updated time to now - pub fn mark_updated(&mut self) { - let now = Instant::now(); - if self.first_updated.is_none() { - self.first_updated = Some(now); - } - self.last_updated = Some(now); - } - - pub fn update_from_note_key( - txn: &Transaction, - ndb: &Ndb, - unknown_ids: &mut UnknownIds, - note_cache: &mut NoteCache, - key: NoteKey, - ) -> bool { - let note = if let Ok(note) = ndb.get_note_by_key(txn, key) { - note - } else { - return false; - }; - - UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, ¬e) - } - - /// Should be called on freshly polled notes from subscriptions - pub fn update_from_note_refs( - txn: &Transaction, - ndb: &Ndb, - unknown_ids: &mut UnknownIds, - note_cache: &mut NoteCache, - note_refs: &[NoteRef], - ) { - for note_ref in note_refs { - Self::update_from_note_key(txn, ndb, unknown_ids, note_cache, note_ref.key); - } - } - - pub fn update_from_note( - txn: &Transaction, - ndb: &Ndb, - unknown_ids: &mut UnknownIds, - note_cache: &mut NoteCache, - note: &Note, - ) -> bool { - let before = unknown_ids.ids().len(); - let key = note.key().expect("note key"); - //let cached_note = note_cache.cached_note_or_insert(key, note).clone(); - let cached_note = note_cache.cached_note_or_insert(key, note); - if let Err(e) = get_unknown_note_ids(ndb, cached_note, txn, note, unknown_ids.ids_mut()) { - error!("UnknownIds::update_from_note {e}"); - } - let after = unknown_ids.ids().len(); - - if before != after { - unknown_ids.mark_updated(); - true - } else { - false - } - } - - pub fn add_unknown_id_if_missing(&mut self, ndb: &Ndb, txn: &Transaction, unk_id: &UnknownId) { - match unk_id { - UnknownId::Pubkey(pk) => self.add_pubkey_if_missing(ndb, txn, pk), - UnknownId::Id(note_id) => self.add_note_id_if_missing(ndb, txn, note_id), - } - } - - pub fn add_pubkey_if_missing(&mut self, ndb: &Ndb, txn: &Transaction, pubkey: &Pubkey) { - // we already have this profile, skip - if ndb.get_profile_by_pubkey(txn, pubkey).is_ok() { - return; - } - - self.ids.insert(UnknownId::Pubkey(*pubkey)); - self.mark_updated(); - } - - pub fn add_note_id_if_missing(&mut self, ndb: &Ndb, txn: &Transaction, note_id: &NoteId) { - // we already have this note, skip - if ndb.get_note_by_id(txn, note_id.bytes()).is_ok() { - return; - } - - self.ids.insert(UnknownId::Id(*note_id)); - self.mark_updated(); - } - - pub fn update( - txn: &Transaction, - unknown_ids: &mut UnknownIds, - columns: &Columns, - ndb: &Ndb, - note_cache: &mut NoteCache, - ) -> bool { - let before = unknown_ids.ids().len(); - if let Err(e) = get_unknown_ids(txn, unknown_ids, columns, ndb, note_cache) { - error!("UnknownIds::update {e}"); - } - let after = unknown_ids.ids().len(); - - if before != after { - unknown_ids.mark_updated(); - true - } else { - false - } - } -} - -#[derive(Hash, Clone, Copy, PartialEq, Eq)] -pub enum UnknownId { - Pubkey(Pubkey), - Id(NoteId), -} - -impl UnknownId { - pub fn is_pubkey(&self) -> Option<&Pubkey> { - match self { - UnknownId::Pubkey(pk) => Some(pk), - _ => None, - } - } - - pub fn is_id(&self) -> Option<&NoteId> { - match self { - UnknownId::Id(id) => Some(id), - _ => None, - } - } -} - -/// Look for missing notes in various parts of notes that we see: -/// -/// - pubkeys and notes mentioned inside the note -/// - notes being replied to -/// -/// We return all of this in a HashSet so that we can fetch these from -/// remote relays. -/// -pub fn get_unknown_note_ids<'a>( +pub fn update_from_columns( + txn: &Transaction, + unknown_ids: &mut UnknownIds, + columns: &Columns, ndb: &Ndb, - cached_note: &CachedNote, - txn: &'a Transaction, - note: &Note<'a>, - ids: &mut HashSet, -) -> Result<()> { - #[cfg(feature = "profiling")] - puffin::profile_function!(); - - // the author pubkey - if ndb.get_profile_by_pubkey(txn, note.pubkey()).is_err() { - ids.insert(UnknownId::Pubkey(Pubkey::new(*note.pubkey()))); + note_cache: &mut NoteCache, +) -> bool { + let before = unknown_ids.ids().len(); + if let Err(e) = get_unknown_ids(txn, unknown_ids, columns, ndb, note_cache) { + error!("UnknownIds::update {e}"); } + let after = unknown_ids.ids().len(); - // pull notes that notes are replying to - if cached_note.reply.root.is_some() { - let note_reply = cached_note.reply.borrow(note.tags()); - if let Some(root) = note_reply.root() { - if ndb.get_note_by_id(txn, root.id).is_err() { - ids.insert(UnknownId::Id(NoteId::new(*root.id))); - } - } - - if !note_reply.is_reply_to_root() { - if let Some(reply) = note_reply.reply() { - if ndb.get_note_by_id(txn, reply.id).is_err() { - ids.insert(UnknownId::Id(NoteId::new(*reply.id))); - } - } - } + if before != after { + unknown_ids.mark_updated(); + true + } else { + false } - - let blocks = ndb.get_blocks_by_key(txn, note.key().expect("note key"))?; - for block in blocks.iter(note) { - if block.blocktype() != BlockType::MentionBech32 { - continue; - } - - match block.as_mention().unwrap() { - Mention::Pubkey(npub) => { - if ndb.get_profile_by_pubkey(txn, npub.pubkey()).is_err() { - ids.insert(UnknownId::Pubkey(Pubkey::new(*npub.pubkey()))); - } - } - Mention::Profile(nprofile) => { - if ndb.get_profile_by_pubkey(txn, nprofile.pubkey()).is_err() { - ids.insert(UnknownId::Pubkey(Pubkey::new(*nprofile.pubkey()))); - } - } - Mention::Event(ev) => match ndb.get_note_by_id(txn, ev.id()) { - Err(_) => { - ids.insert(UnknownId::Id(NoteId::new(*ev.id()))); - if let Some(pk) = ev.pubkey() { - if ndb.get_profile_by_pubkey(txn, pk).is_err() { - ids.insert(UnknownId::Pubkey(Pubkey::new(*pk))); - } - } - } - Ok(note) => { - if ndb.get_profile_by_pubkey(txn, note.pubkey()).is_err() { - ids.insert(UnknownId::Pubkey(Pubkey::new(*note.pubkey()))); - } - } - }, - Mention::Note(note) => match ndb.get_note_by_id(txn, note.id()) { - Err(_) => { - ids.insert(UnknownId::Id(NoteId::new(*note.id()))); - } - Ok(note) => { - if ndb.get_profile_by_pubkey(txn, note.pubkey()).is_err() { - ids.insert(UnknownId::Pubkey(Pubkey::new(*note.pubkey()))); - } - } - }, - _ => {} - } - } - - Ok(()) } -fn get_unknown_ids( +pub fn get_unknown_ids( txn: &Transaction, unknown_ids: &mut UnknownIds, columns: &Columns, @@ -375,7 +49,13 @@ fn get_unknown_ids( new_cached_note }; - let _ = get_unknown_note_ids(ndb, &cached_note, txn, ¬e, unknown_ids.ids_mut()); + let _ = notedeck::get_unknown_note_ids( + ndb, + &cached_note, + txn, + ¬e, + unknown_ids.ids_mut(), + ); } } @@ -387,31 +67,3 @@ fn get_unknown_ids( Ok(()) } - -fn get_unknown_ids_filter(ids: &[&UnknownId]) -> Option> { - if ids.is_empty() { - return None; - } - - let ids = &ids[0..500.min(ids.len())]; - let mut filters: Vec = vec![]; - - let pks: Vec<&[u8; 32]> = ids - .iter() - .flat_map(|id| id.is_pubkey().map(|pk| pk.bytes())) - .collect(); - if !pks.is_empty() { - let pk_filter = Filter::new().authors(pks).kinds([0]).build(); - filters.push(pk_filter); - } - - let note_ids: Vec<&[u8; 32]> = ids - .iter() - .flat_map(|id| id.is_id().map(|id| id.bytes())) - .collect(); - if !note_ids.is_empty() { - filters.push(Filter::new().ids(note_ids).build()); - } - - Some(filters) -}