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) -}