Merge pull request 'Add sqlite' (#4) from sqlite into main
Some checks failed
Rust CI / build-and-test (macOS-latest) (push) Has been cancelled
Rust CI / build-and-test (ubuntu-latest) (push) Has been cancelled
Rust CI / build-and-test (windows-latest) (push) Has been cancelled
Rust CI / cross-compile (macOS-latest, x86_64-pc-windows-gnu) (push) Has been cancelled
Rust CI / cross-compile (macOS-latest, x86_64-unknown-linux-gnu) (push) Has been cancelled
Rust CI / cross-compile (ubuntu-latest, x86_64-pc-windows-gnu) (push) Has been cancelled
Some checks failed
Rust CI / build-and-test (macOS-latest) (push) Has been cancelled
Rust CI / build-and-test (ubuntu-latest) (push) Has been cancelled
Rust CI / build-and-test (windows-latest) (push) Has been cancelled
Rust CI / cross-compile (macOS-latest, x86_64-pc-windows-gnu) (push) Has been cancelled
Rust CI / cross-compile (macOS-latest, x86_64-unknown-linux-gnu) (push) Has been cancelled
Rust CI / cross-compile (ubuntu-latest, x86_64-pc-windows-gnu) (push) Has been cancelled
Reviewed-on: #4
This commit is contained in:
commit
4df83c9bc2
7 changed files with 463 additions and 158 deletions
170
Cargo.lock
generated
170
Cargo.lock
generated
|
@ -1,6 +1,6 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ab_glyph"
|
||||
|
@ -171,6 +171,12 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
|
@ -182,9 +188,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.82"
|
||||
version = "1.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
|
||||
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
|
@ -826,13 +832,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.95"
|
||||
version = "1.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
|
||||
checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -896,6 +902,18 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"num-traits",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
|
@ -1575,6 +1593,18 @@ dependencies = [
|
|||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.9.0"
|
||||
|
@ -1937,6 +1967,15 @@ dependencies = [
|
|||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hassle-rs"
|
||||
version = "0.11.0"
|
||||
|
@ -2013,16 +2052,20 @@ dependencies = [
|
|||
name = "hoot-app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"eframe",
|
||||
"egui_extras",
|
||||
"egui_tabs",
|
||||
"ewebsock",
|
||||
"image 0.25.1",
|
||||
"include_dir",
|
||||
"nostr",
|
||||
"pollster",
|
||||
"puffin",
|
||||
"puffin_http",
|
||||
"rand",
|
||||
"rusqlite",
|
||||
"rusqlite_migration",
|
||||
"security-framework",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -2048,6 +2091,29 @@ version = "1.9.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icrate"
|
||||
version = "0.0.4"
|
||||
|
@ -2127,6 +2193,25 @@ version = "1.10.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126"
|
||||
|
||||
[[package]]
|
||||
name = "include_dir"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
|
||||
dependencies = [
|
||||
"include_dir_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir_macros"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.2.6"
|
||||
|
@ -2338,6 +2423,18 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.3.8"
|
||||
|
@ -2803,6 +2900,28 @@ version = "0.3.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "300.4.2+3.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "168ce4e058f975fe43e89d9ccf78ca668601887ae736090aacc23ae353c298e2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"openssl-src",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "orbclient"
|
||||
version = "0.3.47"
|
||||
|
@ -3360,6 +3479,33 @@ version = "0.19.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.32.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"chrono",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite_migration"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "923b42e802f7dc20a0a6b5e097ba7c83fe4289da07e49156fecf6af08aa9cd1c"
|
||||
dependencies = [
|
||||
"include_dir",
|
||||
"log",
|
||||
"rusqlite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
|
@ -3613,6 +3759,12 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
|
@ -4277,6 +4429,12 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
|
|
21
Cargo.toml
21
Cargo.toml
|
@ -6,15 +6,16 @@ edition = "2021"
|
|||
publish = false
|
||||
|
||||
[features]
|
||||
profiling = ["dep:puffin", "dep:puffin_http", "eframe/puffin", "egui_extras/puffin"]
|
||||
profiling = [
|
||||
"dep:puffin",
|
||||
"dep:puffin_http",
|
||||
"eframe/puffin",
|
||||
"egui_extras/puffin",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
eframe = { version = "0.27.2", features = ["default", "persistence"] }
|
||||
egui_extras = { version = "0.27.2", features = [
|
||||
"file",
|
||||
"image",
|
||||
"svg",
|
||||
] }
|
||||
egui_extras = { version = "0.27.2", features = ["file", "image", "svg"] }
|
||||
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "120971fc43db6ba0b6f194f4bd4a66f7e00a4e22" }
|
||||
image = { version = "0.25.1", features = ["jpeg", "png"] }
|
||||
tracing = "0.1.40"
|
||||
|
@ -28,6 +29,14 @@ nostr = { version = "0.37.0", features = ["std", "nip59"] }
|
|||
serde = "1.0.204"
|
||||
serde_json = "1.0.121"
|
||||
pollster = "0.4.0"
|
||||
rusqlite = { version = "0.32.1", features = [
|
||||
"chrono",
|
||||
"serde_json",
|
||||
"bundled-sqlcipher-vendored-openssl",
|
||||
] }
|
||||
rusqlite_migration = { version = "1.3.1", features = ["from-directory"] }
|
||||
anyhow = "1.0.96"
|
||||
include_dir = "0.7.4"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
security-framework = "3.0.0"
|
||||
|
|
14
migrations/001-nostr_events/up.sql
Normal file
14
migrations/001-nostr_events/up.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
CREATE TABLE IF NOT EXISTS events (
|
||||
id TEXT PRIMARY KEY,
|
||||
pubkey TEXT NOT NULL GENERATED ALWAYS AS (jsonb_extract (raw, '$.pubkey')),
|
||||
created_at INTEGER NOT NULL GENERATED ALWAYS AS (jsonb_extract (raw, '$.created_at')) VIRTUAL,
|
||||
kind INTEGER NOT NULL GENERATED ALWAYS AS (jsonb_extract (raw, '$.kind')) VIRTUAL,
|
||||
tags BLOB NOT NULL GENERATED ALWAYS AS (jsonb_extract (raw, '$.tags')) VIRTUAL,
|
||||
content TEXT NOT NULL GENERATED ALWAYS AS (jsonb_extract (raw, '$.content')) VIRTUAL,
|
||||
sig TEXT GENERATED ALWAYS AS (jsonb_extract (raw, '$.sig')) VIRTUAL,
|
||||
raw BLOB NOT NULL
|
||||
);
|
||||
|
||||
-- indexes
|
||||
CREATE INDEX idx_events_pubkey ON events (pubkey);
|
||||
CREATE INDEX idx_events_kind ON events (created_at);
|
109
src/db.rs
Normal file
109
src/db.rs
Normal file
|
@ -0,0 +1,109 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use include_dir::{include_dir, Dir};
|
||||
use nostr::{Event, Kind};
|
||||
use rusqlite::Connection;
|
||||
use rusqlite_migration::{Migrations, M};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::account_manager::AccountManager;
|
||||
use crate::mail_event::MAIL_EVENT_KIND;
|
||||
|
||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
||||
|
||||
static MIGRATIONS: LazyLock<Migrations<'static>> =
|
||||
LazyLock::new(|| Migrations::from_directory(&MIGRATIONS_DIR).unwrap());
|
||||
|
||||
pub struct Db {
|
||||
connection: Connection,
|
||||
}
|
||||
|
||||
impl Db {
|
||||
pub fn new(path: PathBuf) -> Result<Self> {
|
||||
let mut conn = Connection::open(path)?;
|
||||
|
||||
// Apply migrations
|
||||
MIGRATIONS.to_latest(&mut conn)?;
|
||||
|
||||
Ok(Self { connection: conn })
|
||||
}
|
||||
|
||||
pub fn store_event(
|
||||
&self,
|
||||
event: &Event,
|
||||
account_manager: &mut AccountManager,
|
||||
) -> Result<()> {
|
||||
// Try to unwrap the gift wrap if this event is a gift wrap
|
||||
let store_unwrapped =
|
||||
is_gift_wrap(event) && account_manager.unwrap_gift_wrap(event).is_ok();
|
||||
|
||||
if store_unwrapped {
|
||||
let unwrapped = account_manager.unwrap_gift_wrap(event).unwrap();
|
||||
let mut rumor = unwrapped.rumor.clone();
|
||||
rumor.ensure_id();
|
||||
|
||||
let id = rumor.id.expect("Invalid Gift Wrapped Event: There is no ID!").to_hex();
|
||||
let raw = json!(rumor).to_string();
|
||||
|
||||
self.connection.execute(
|
||||
"INSERT INTO events (id, raw)
|
||||
VALUES (?1, ?2)",
|
||||
(id, raw),
|
||||
)?;
|
||||
} else {
|
||||
let id = event.id.to_string();
|
||||
let raw = json!(event).to_string();
|
||||
|
||||
self.connection.execute(
|
||||
"INSERT INTO events (id, raw)
|
||||
VALUES (?1, ?2)",
|
||||
(id, raw),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_event(&self, event_id: &str) -> Result<bool> {
|
||||
let count: i64 = self.connection.query_row(
|
||||
"SELECT COUNT(*) FROM events WHERE id = ?",
|
||||
[event_id],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
/// Get all event IDs for mail events
|
||||
pub fn get_mail_event_ids(&self) -> Result<Vec<String>> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT id FROM events WHERE kind = ?")?;
|
||||
|
||||
let mail_kind = u32::from(MAIL_EVENT_KIND as u16);
|
||||
|
||||
let id_iter = stmt.query_map([mail_kind], |row| {
|
||||
let id: String = row.get(0)?;
|
||||
Ok(id)
|
||||
})?;
|
||||
|
||||
let mut ids = Vec::new();
|
||||
for id_result in id_iter {
|
||||
match id_result {
|
||||
Ok(id) => ids.push(id),
|
||||
Err(e) => {
|
||||
tracing::error!("Error loading mail event ID: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ids)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an event is a gift wrap
|
||||
fn is_gift_wrap(event: &Event) -> bool {
|
||||
event.kind == Kind::GiftWrap
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
use nostr::{Event, EventBuilder, Keys, Kind, PublicKey, Tag, TagKind, TagStandard};
|
||||
use std::collections::HashMap;
|
||||
use pollster::FutureExt as _;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub const MAIL_EVENT_KIND: u16 = 1059;
|
||||
pub const MAIL_EVENT_KIND: u16 = 2024;
|
||||
|
||||
pub struct MailMessage {
|
||||
pub to: Vec<PublicKey>,
|
||||
|
@ -23,19 +23,25 @@ impl MailMessage {
|
|||
}
|
||||
|
||||
for pubkey in &self.cc {
|
||||
tags.push(Tag::custom(TagKind::p(), vec![pubkey.to_hex().as_str(), "cc"]));
|
||||
tags.push(Tag::custom(
|
||||
TagKind::p(),
|
||||
vec![pubkey.to_hex().as_str(), "cc"],
|
||||
));
|
||||
pubkeys_to_send_to.push(*pubkey);
|
||||
}
|
||||
|
||||
tags.push(Tag::from_standardized(TagStandard::Subject(self.subject.clone())));
|
||||
tags.push(Tag::from_standardized(TagStandard::Subject(
|
||||
self.subject.clone(),
|
||||
)));
|
||||
|
||||
let base_event = EventBuilder::new(Kind::Custom(MAIL_EVENT_KIND), &self.content)
|
||||
.tags(tags);
|
||||
let base_event = EventBuilder::new(Kind::Custom(MAIL_EVENT_KIND), &self.content).tags(tags);
|
||||
|
||||
let mut event_list: HashMap<PublicKey, Event> = HashMap::new();
|
||||
for pubkey in pubkeys_to_send_to {
|
||||
let wrapped_event =
|
||||
EventBuilder::gift_wrap(sending_keys, &pubkey, base_event.clone(), None).block_on().unwrap();
|
||||
EventBuilder::gift_wrap(sending_keys, &pubkey, base_event.clone(), None)
|
||||
.block_on()
|
||||
.unwrap();
|
||||
event_list.insert(pubkey, wrapped_event);
|
||||
}
|
||||
|
||||
|
|
216
src/main.rs
216
src/main.rs
|
@ -5,9 +5,11 @@ use egui::FontFamily::Proportional;
|
|||
use egui_extras::{Column, TableBuilder};
|
||||
use relay::RelayMessage;
|
||||
use std::collections::HashMap;
|
||||
use nostr::{SingleLetterTag, TagKind};
|
||||
use tracing::{debug, error, info, Level};
|
||||
|
||||
mod account_manager;
|
||||
mod db;
|
||||
mod error;
|
||||
mod keystorage;
|
||||
mod mail_event;
|
||||
|
@ -84,6 +86,7 @@ pub struct Hoot {
|
|||
relays: relay::RelayPool,
|
||||
events: Vec<nostr::Event>,
|
||||
account_manager: account_manager::AccountManager,
|
||||
db: db::Db,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
@ -113,7 +116,17 @@ fn update_app(app: &mut Hoot, ctx: &egui::Context) {
|
|||
if app.account_manager.loaded_keys.len() > 0 {
|
||||
let mut gw_sub = relay::Subscription::default();
|
||||
|
||||
let filter = nostr::Filter::new().kind(nostr::Kind::Custom(mail_event::MAIL_EVENT_KIND)).custom_tag(nostr::SingleLetterTag { character: nostr::Alphabet::P, uppercase: false }, app.account_manager.loaded_keys.clone().into_iter().map(|keys| keys.public_key()));
|
||||
let filter = nostr::Filter::new().kind(nostr::Kind::GiftWrap).custom_tag(
|
||||
nostr::SingleLetterTag {
|
||||
character: nostr::Alphabet::P,
|
||||
uppercase: false,
|
||||
},
|
||||
app.account_manager
|
||||
.loaded_keys
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|keys| keys.public_key()),
|
||||
);
|
||||
gw_sub.filter(filter);
|
||||
|
||||
// TODO: fix error handling
|
||||
|
@ -154,13 +167,30 @@ fn process_event(app: &mut Hoot, _sub_id: &str, event_json: &str) {
|
|||
|
||||
// Parse the event using the RelayMessage type which handles the ["EVENT", subscription_id, event_json] format
|
||||
if let Ok(event) = serde_json::from_str::<nostr::Event>(event_json) {
|
||||
// Verify the event signature
|
||||
if event.verify().is_ok() {
|
||||
debug!("Verified event: {:?}", event);
|
||||
app.events.push(event);
|
||||
} else {
|
||||
error!("Event verification failed");
|
||||
// Check if we already have this event
|
||||
if let Ok(has_event) = app.db.has_event(&event.id.to_string()) {
|
||||
if has_event {
|
||||
debug!("Skipping already stored event: {}", event.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the event signature
|
||||
if event.verify().is_ok() {
|
||||
debug!("Verified event: {:?}", event);
|
||||
|
||||
// Store the event in memory
|
||||
app.events.push(event.clone());
|
||||
|
||||
// Store the event in the database
|
||||
if let Err(e) = app.db.store_event(&event, &mut app.account_manager) {
|
||||
error!("Failed to store event in database: {}", e);
|
||||
} else {
|
||||
debug!("Successfully stored event with id {} in database", event.id);
|
||||
}
|
||||
} else {
|
||||
error!("Event verification failed for event: {}", event.id);
|
||||
}
|
||||
} else {
|
||||
error!("Failed to parse event JSON: {}", event_json);
|
||||
}
|
||||
|
@ -182,10 +212,13 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
|||
ui.add_space(16.0);
|
||||
|
||||
// Compose button
|
||||
if ui.add_sized(
|
||||
[180.0, 36.0],
|
||||
egui::Button::new("✉ Compose").fill(egui::Color32::from_rgb(149, 117, 205)),
|
||||
).clicked() {
|
||||
if ui
|
||||
.add_sized(
|
||||
[180.0, 36.0],
|
||||
egui::Button::new("✉ Compose").fill(egui::Color32::from_rgb(149, 117, 205)),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
let state = ui::compose_window::ComposeWindowState {
|
||||
subject: String::new(),
|
||||
to_field: String::new(),
|
||||
|
@ -197,9 +230,9 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
|||
.compose_window
|
||||
.insert(egui::Id::new(rand::random::<u32>()), state);
|
||||
}
|
||||
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
|
||||
// Navigation items
|
||||
let nav_items = [
|
||||
("📥 Inbox", Page::Inbox, app.events.len()),
|
||||
|
@ -212,18 +245,35 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
|||
|
||||
for (label, page, count) in nav_items {
|
||||
let is_selected = app.page == page;
|
||||
let response = ui.selectable_label(is_selected, format!("{} {}", label, if count > 0 { count.to_string() } else { String::new() }));
|
||||
let response = ui.selectable_label(
|
||||
is_selected,
|
||||
format!(
|
||||
"{} {}",
|
||||
label,
|
||||
if count > 0 {
|
||||
count.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
),
|
||||
);
|
||||
if response.clicked() {
|
||||
app.page = page;
|
||||
}
|
||||
}
|
||||
if ui.button("onboarding").clicked() {
|
||||
app.page = Page::OnboardingNew;
|
||||
}
|
||||
|
||||
// Add flexible space to push profile to bottom
|
||||
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
|
||||
ui.add_space(8.0);
|
||||
// Profile section
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_sized([32.0, 32.0], egui::Button::new("👤")).clicked() {
|
||||
if ui
|
||||
.add_sized([32.0, 32.0], egui::Button::new("👤"))
|
||||
.clicked()
|
||||
{
|
||||
app.page = Page::Settings;
|
||||
}
|
||||
if let Some(key) = app.account_manager.loaded_keys.first() {
|
||||
|
@ -245,19 +295,19 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
|||
[search_width, 32.0],
|
||||
egui::TextEdit::singleline(&mut String::new())
|
||||
.hint_text("Search")
|
||||
.margin(egui::vec2(8.0, 4.0))
|
||||
.margin(egui::vec2(8.0, 4.0)),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
|
||||
// Email list using TableBuilder
|
||||
TableBuilder::new(ui)
|
||||
.column(Column::auto()) // Checkbox
|
||||
.column(Column::auto()) // Star
|
||||
.column(Column::remainder()) // Sender
|
||||
.column(Column::remainder()) // Content
|
||||
.column(Column::remainder()) // Time
|
||||
.column(Column::auto()) // Checkbox
|
||||
.column(Column::auto()) // Star
|
||||
.column(Column::remainder()) // Sender
|
||||
.column(Column::remainder()) // Content
|
||||
.column(Column::remainder()) // Time
|
||||
.striped(true)
|
||||
.sense(Sense::click())
|
||||
.auto_shrink(Vec2b { x: false, y: false })
|
||||
|
@ -283,37 +333,34 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
|||
let events = app.events.clone();
|
||||
body.rows(row_height, events.len(), |mut row| {
|
||||
let event = &events[row.index()];
|
||||
|
||||
// Try to unwrap the gift wrap
|
||||
if let Ok(unwrapped) = app.account_manager.unwrap_gift_wrap(event) {
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut false, "");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut false, "");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(unwrapped.sender.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
// Try to get subject from tags
|
||||
let subject = match &unwrapped.rumor.tags.find(nostr::TagKind::Subject) {
|
||||
Some(s) => match s.content() {
|
||||
Some(c) => format!("{}: {}", c.to_string(), unwrapped.rumor.content),
|
||||
None => unwrapped.rumor.content.clone(),
|
||||
},
|
||||
None => unwrapped.rumor.content.clone(),
|
||||
};
|
||||
ui.label(subject);
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label("2 minutes ago");
|
||||
});
|
||||
|
||||
if row.response().clicked() {
|
||||
app.focused_post = event.id.to_string();
|
||||
app.page = Page::Post;
|
||||
}
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut false, "");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut false, "");
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(event.pubkey.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
// Try to get subject from tags
|
||||
let subject = match &event.tags.find(nostr::TagKind::Subject) {
|
||||
Some(s) => match s.content() {
|
||||
Some(c) => format!("{}: {}", c.to_string(), event.content),
|
||||
None => event.content.clone(),
|
||||
},
|
||||
None => event.content.clone(),
|
||||
};
|
||||
ui.label(subject);
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label("2 minutes ago");
|
||||
});
|
||||
|
||||
if row.response().clicked() {
|
||||
app.focused_post = event.id.to_string();
|
||||
app.page = Page::Post;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -322,15 +369,25 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
|||
ui::settings::SettingsScreen::ui(app, ui);
|
||||
}
|
||||
Page::Post => {
|
||||
if let Some(event) = app.events.iter().find(|e| e.id.to_string() == app.focused_post) {
|
||||
if let Some(event) = app
|
||||
.events
|
||||
.iter()
|
||||
.find(|e| e.id.to_string() == app.focused_post)
|
||||
{
|
||||
if let Ok(unwrapped) = app.account_manager.unwrap_gift_wrap(event) {
|
||||
// Message header section
|
||||
ui.add_space(8.0);
|
||||
ui.heading(&unwrapped.rumor.tags.find(nostr::TagKind::Subject)
|
||||
let subject = &unwrapped
|
||||
.rumor
|
||||
.tags
|
||||
.find(nostr::TagKind::Subject)
|
||||
.and_then(|s| s.content())
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or_else(|| "No Subject".to_string()));
|
||||
|
||||
.unwrap_or_else(|| "No Subject".to_string());
|
||||
// Message header section
|
||||
ui.add_space(8.0);
|
||||
ui.heading(subject);
|
||||
let destination: Vec<&nostr::PublicKey> = unwrapped.rumor.tags.public_keys().collect();
|
||||
let destination_stringed = destination.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(" ");
|
||||
|
||||
// Metadata grid
|
||||
egui::Grid::new("email_metadata")
|
||||
.num_columns(2)
|
||||
|
@ -341,11 +398,7 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
|||
ui.end_row();
|
||||
|
||||
ui.label("To");
|
||||
ui.label(unwrapped.rumor.tags.iter()
|
||||
.filter_map(|tag| tag.content())
|
||||
.next()
|
||||
.unwrap_or_else(|| "Unknown")
|
||||
.to_string());
|
||||
ui.label(destination_stringed.clone());
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
|
@ -363,7 +416,16 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
|||
// TODO: Handle delete
|
||||
}
|
||||
if ui.button("↩️ Reply").clicked() {
|
||||
// TODO: Handle reply
|
||||
let state = ui::compose_window::ComposeWindowState {
|
||||
subject: format!("Re: {}", subject),
|
||||
to_field: unwrapped.sender.to_string(),
|
||||
content: String::new(),
|
||||
selected_account: None,
|
||||
minimized: false,
|
||||
};
|
||||
app.state
|
||||
.compose_window
|
||||
.insert(egui::Id::new(rand::random::<u32>()), state);
|
||||
}
|
||||
if ui.button("↪️ Forward").clicked() {
|
||||
// TODO: Handle forward
|
||||
|
@ -387,8 +449,7 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
|||
ui.label("Your draft messages will appear here");
|
||||
}
|
||||
_ => {
|
||||
ui.heading("Coming Soon");
|
||||
ui.label("This feature is under development");
|
||||
ui::onboarding::OnboardingScreen::ui(app, ui);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -396,7 +457,25 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
|
|||
|
||||
impl Hoot {
|
||||
fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
let storage_dir = eframe::storage_dir("Hoot").unwrap();
|
||||
// Create storage directory if it doesn't exist
|
||||
let storage_dir = eframe::storage_dir("hoot").unwrap();
|
||||
std::fs::create_dir_all(&storage_dir).unwrap();
|
||||
|
||||
// Create the database file path
|
||||
let db_path = storage_dir.join("hoot.db");
|
||||
|
||||
// Initialize the database
|
||||
let db = match db::Db::new(db_path) {
|
||||
Ok(db) => {
|
||||
info!("Database initialized successfully");
|
||||
db
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to initialize database: {}", e);
|
||||
panic!("Database initialization failed: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
page: Page::Inbox,
|
||||
focused_post: "".into(),
|
||||
|
@ -405,6 +484,7 @@ impl Hoot {
|
|||
relays: relay::RelayPool::new(),
|
||||
events: Vec::new(),
|
||||
account_manager: account_manager::AccountManager::new(),
|
||||
db,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,85 +22,20 @@ impl ComposeWindow {
|
|||
let min_height = screen_rect.height().min(400.0);
|
||||
|
||||
// First collect all window IDs and their minimized state
|
||||
let window_states: Vec<_> = app.state.compose_window.iter()
|
||||
.map(|(id, state)| (*id, state.minimized))
|
||||
.collect();
|
||||
|
||||
let mut should_close = false;
|
||||
|
||||
let state = app
|
||||
.state
|
||||
.compose_window
|
||||
.get_mut(&id)
|
||||
.expect("no state found for id");
|
||||
|
||||
if state.minimized {
|
||||
// Count how many minimized windows are before this one
|
||||
let minimized_index = window_states.iter()
|
||||
.filter(|(other_id, is_minimized)| {
|
||||
*is_minimized && other_id.value() < id.value()
|
||||
})
|
||||
.count();
|
||||
|
||||
let minimized_height = 32.0;
|
||||
let minimized_width = 200.0;
|
||||
let minimized_spacing = 2.0;
|
||||
let stack_on_left = screen_rect.width() < min_width + minimized_width + 40.0;
|
||||
|
||||
let window_y_offset = -(minimized_height + minimized_spacing) * (minimized_index as f32 + 1.0);
|
||||
|
||||
let (anchor, anchor_pos) = if stack_on_left {
|
||||
(egui::Align2::LEFT_BOTTOM, [20.0, window_y_offset])
|
||||
} else {
|
||||
(egui::Align2::RIGHT_BOTTOM, [-20.0, window_y_offset])
|
||||
};
|
||||
|
||||
egui::Window::new("📧")
|
||||
.id(id)
|
||||
.fixed_size([minimized_width, minimized_height])
|
||||
.anchor(anchor, anchor_pos)
|
||||
.title_bar(true)
|
||||
.frame(egui::Frame::window(&ctx.style()).multiply_with_opacity(0.95))
|
||||
.show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("📧").clicked() {
|
||||
state.minimized = false;
|
||||
}
|
||||
let subject = state.subject.as_str();
|
||||
let display_text = if subject.is_empty() {
|
||||
"New Message"
|
||||
} else {
|
||||
subject.get(0..20).unwrap_or(subject)
|
||||
};
|
||||
ui.label(RichText::new(display_text).size(11.0));
|
||||
});
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
egui::Window::new("New Message")
|
||||
.id(id)
|
||||
.default_size([min_width, min_height])
|
||||
.min_width(300.0)
|
||||
.min_height(200.0)
|
||||
.anchor(egui::Align2::RIGHT_BOTTOM, [-20.0, -20.0])
|
||||
.default_pos([screen_rect.right() - min_width - 20.0, screen_rect.bottom() - min_height - 20.0])
|
||||
.collapsible(false)
|
||||
.resizable(true)
|
||||
.show(ctx, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
// Window controls at the top
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.button("❌").clicked() {
|
||||
should_close = true;
|
||||
}
|
||||
if ui.button("🗕").clicked() {
|
||||
state.minimized = true;
|
||||
}
|
||||
ui.add_space(ui.available_width() - 50.0); // Push buttons to the right
|
||||
});
|
||||
});
|
||||
|
||||
// Header section
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("To:");
|
||||
|
@ -177,7 +112,6 @@ impl ComposeWindow {
|
|||
let events_to_send =
|
||||
msg.to_events(&state.selected_account.clone().unwrap());
|
||||
|
||||
info!("new events! {:?}", events_to_send);
|
||||
// send over wire
|
||||
for event in events_to_send {
|
||||
match serde_json::to_string(&ClientMessage::Event { event: event.1 }) {
|
||||
|
@ -218,11 +152,6 @@ impl ComposeWindow {
|
|||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if should_close {
|
||||
app.state.compose_window.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the original show method for backward compatibility
|
||||
|
|
Loading…
Add table
Reference in a new issue