From 21301cc60335a3944689a602a64fd40e5d0b8257 Mon Sep 17 00:00:00 2001
From: Jack Chakany <jack@chakany.systems>
Date: Sat, 1 Mar 2025 13:34:09 -0500
Subject: [PATCH 1/5] add sqlite dependencies

---
 Cargo.lock | 150 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 Cargo.toml |   1 +
 2 files changed, 146 insertions(+), 5 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 582512e..c8889c1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
@@ -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"
@@ -1630,6 +1660,12 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
+[[package]]
+name = "foldhash"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
+
 [[package]]
 name = "foreign-types"
 version = "0.5.0"
@@ -1905,7 +1941,7 @@ checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c"
 dependencies = [
  "bitflags 2.6.0",
  "gpu-descriptor-types",
- "hashbrown",
+ "hashbrown 0.14.3",
 ]
 
 [[package]]
@@ -1937,6 +1973,24 @@ dependencies = [
  "allocator-api2",
 ]
 
+[[package]]
+name = "hashbrown"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.2",
+]
+
 [[package]]
 name = "hassle-rs"
 version = "0.11.0"
@@ -2023,6 +2077,7 @@ dependencies = [
  "puffin",
  "puffin_http",
  "rand",
+ "rusqlite",
  "security-framework",
  "serde",
  "serde_json",
@@ -2048,6 +2103,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"
@@ -2134,7 +2212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
 dependencies = [
  "equivalent",
- "hashbrown",
+ "hashbrown 0.14.3",
 ]
 
 [[package]]
@@ -2338,6 +2416,18 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "libsqlite3-sys"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8935b44e7c13394a179a438e0cebba0fe08fe01b54f152e29a93b5cf993fd4"
+dependencies = [
+ "cc",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.3.8"
@@ -2803,6 +2893,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 +3472,22 @@ version = "0.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
 
+[[package]]
+name = "rusqlite"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c6d5e5acb6f6129fe3f7ba0a7fc77bca1942cb568535e18e7bc40262baf3110"
+dependencies = [
+ "bitflags 2.6.0",
+ "chrono",
+ "fallible-iterator",
+ "fallible-streaming-iterator",
+ "hashlink",
+ "libsqlite3-sys",
+ "serde_json",
+ "smallvec",
+]
+
 [[package]]
 name = "rustc-hash"
 version = "1.1.0"
@@ -3613,6 +3741,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 +4411,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"
diff --git a/Cargo.toml b/Cargo.toml
index 989102f..60d030f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,6 +28,7 @@ nostr = { version = "0.37.0", features = ["std", "nip59"] }
 serde = "1.0.204"
 serde_json = "1.0.121"
 pollster = "0.4.0"
+rusqlite = { version = "0.33.0", features = ["chrono", "serde_json", "bundled-sqlcipher-vendored-openssl"] }
 
 [target.'cfg(target_os = "macos")'.dependencies]
 security-framework = "3.0.0"

From a2deb4145be0a8d4e97e57b354df9dc7910da4f1 Mon Sep 17 00:00:00 2001
From: Jack Chakany <jack@chakany.systems>
Date: Sat, 1 Mar 2025 17:37:56 -0500
Subject: [PATCH 2/5] adding mail events into sqlite

---
 Cargo.lock                         |  72 ++++++----
 Cargo.toml                         |  22 ++-
 migrations/001-nostr_events/up.sql |   9 ++
 src/db.rs                          | 151 ++++++++++++++++++++
 src/mail_event.rs                  |  20 ++-
 src/main.rs                        | 212 ++++++++++++++++++++---------
 6 files changed, 378 insertions(+), 108 deletions(-)
 create mode 100644 migrations/001-nostr_events/up.sql
 create mode 100644 src/db.rs

diff --git a/Cargo.lock b/Cargo.lock
index c8889c1..8dc9e07 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
@@ -188,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"
@@ -1660,12 +1660,6 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
-[[package]]
-name = "foldhash"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
-
 [[package]]
 name = "foreign-types"
 version = "0.5.0"
@@ -1941,7 +1935,7 @@ checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c"
 dependencies = [
  "bitflags 2.6.0",
  "gpu-descriptor-types",
- "hashbrown 0.14.3",
+ "hashbrown",
 ]
 
 [[package]]
@@ -1973,22 +1967,13 @@ dependencies = [
  "allocator-api2",
 ]
 
-[[package]]
-name = "hashbrown"
-version = "0.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
-dependencies = [
- "foldhash",
-]
-
 [[package]]
 name = "hashlink"
-version = "0.10.0"
+version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
 dependencies = [
- "hashbrown 0.15.2",
+ "hashbrown",
 ]
 
 [[package]]
@@ -2067,17 +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",
@@ -2205,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"
@@ -2212,7 +2219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
 dependencies = [
  "equivalent",
- "hashbrown 0.14.3",
+ "hashbrown",
 ]
 
 [[package]]
@@ -2418,9 +2425,9 @@ dependencies = [
 
 [[package]]
 name = "libsqlite3-sys"
-version = "0.31.0"
+version = "0.30.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad8935b44e7c13394a179a438e0cebba0fe08fe01b54f152e29a93b5cf993fd4"
+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
 dependencies = [
  "cc",
  "openssl-sys",
@@ -3474,9 +3481,9 @@ checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
 
 [[package]]
 name = "rusqlite"
-version = "0.33.0"
+version = "0.32.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c6d5e5acb6f6129fe3f7ba0a7fc77bca1942cb568535e18e7bc40262baf3110"
+checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
 dependencies = [
  "bitflags 2.6.0",
  "chrono",
@@ -3488,6 +3495,17 @@ dependencies = [
  "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"
diff --git a/Cargo.toml b/Cargo.toml
index 60d030f..4e5d609 100644
--- a/Cargo.toml
+++ b/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,7 +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.33.0", features = ["chrono", "serde_json", "bundled-sqlcipher-vendored-openssl"] }
+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"
diff --git a/migrations/001-nostr_events/up.sql b/migrations/001-nostr_events/up.sql
new file mode 100644
index 0000000..d1852ec
--- /dev/null
+++ b/migrations/001-nostr_events/up.sql
@@ -0,0 +1,9 @@
+CREATE TABLE IF NOT EXISTS events (
+    id TEXT PRIMARY KEY,
+    pubkey TEXT NOT NULL,
+    created_at INTEGER NOT NULL,
+    kind INTEGER NOT NULL,
+    tags TEXT NOT NULL CHECK (json_valid (tags)),
+    content TEXT NOT NULL,
+    sig TEXT
+);
diff --git a/src/db.rs b/src/db.rs
new file mode 100644
index 0000000..dc47ca5
--- /dev/null
+++ b/src/db.rs
@@ -0,0 +1,151 @@
+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 })
+    }
+
+    /// Store a mail event in the database
+    ///
+    /// This function first attempts to unwrap the gift wrap if necessary,
+    /// and then stores the event in the database.
+    pub fn store_mail_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();
+
+        // Determine what event to store
+        if store_unwrapped {
+            // Unwrap succeeded, store the unwrapped event
+            let unwrapped = account_manager.unwrap_gift_wrap(event).unwrap();
+
+            // Get event details from the unwrapped gift
+            let id = match unwrapped.rumor.id {
+                Some(id) => id.to_string(),
+                None => "unknown".to_string(),
+            };
+            let pubkey = unwrapped.rumor.pubkey.to_string();
+            let created_at = unwrapped.rumor.created_at.as_u64();
+            let kind = unwrapped.rumor.kind.as_u16() as u32;
+            let tags_json = json!(unwrapped.rumor.tags).to_string();
+            let content = unwrapped.rumor.content.clone();
+            let sig = unwrapped.sender.to_string(); // Use sender pubkey as signature reference
+
+            // Store the unwrapped event in the database
+            self.connection.execute(
+                "INSERT OR REPLACE INTO events (id, pubkey, created_at, kind, tags, content, sig)
+                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
+                (id, pubkey, created_at, kind, tags_json, content, sig),
+            )?;
+        } else {
+            // Use original event
+            // Convert tags to JSON string for storage
+            let tags_json = json!(event.tags).to_string();
+
+            // Get event details
+            let id = event.id.to_string();
+            let pubkey = event.pubkey.to_string();
+            let created_at = event.created_at.as_u64();
+            let kind = event.kind.as_u16() as u32;
+            let sig = event.sig.to_string();
+
+            // Store the event in the database
+            self.connection.execute(
+                "INSERT OR REPLACE INTO events (id, pubkey, created_at, kind, tags, content, sig)
+                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
+                (id, pubkey, created_at, kind, tags_json, &event.content, sig),
+            )?;
+        }
+
+        Ok(())
+    }
+
+    /// Check if the database contains an event with the given ID
+    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)
+    }
+
+    /// Get the JSON representation of an event by its ID
+    pub fn get_event_json(&self, event_id: &str) -> Result<Option<String>> {
+        let result = self.connection.query_row(
+            "SELECT json_object('id', id, 'pubkey', pubkey, 'created_at', created_at,
+                              'kind', kind, 'tags', json(tags), 'content', content,
+                              'sig', sig)
+             FROM events WHERE id = ?",
+            [event_id],
+            |row| row.get::<_, String>(0),
+        );
+
+        match result {
+            Ok(json) => Ok(Some(json)),
+            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
+            Err(e) => Err(e.into()),
+        }
+    }
+}
+
+/// Check if an event is a gift wrap
+fn is_gift_wrap(event: &Event) -> bool {
+    event.kind == Kind::GiftWrap
+}
diff --git a/src/mail_event.rs b/src/mail_event.rs
index 21e8e47..bfc2142 100644
--- a/src/mail_event.rs
+++ b/src/mail_event.rs
@@ -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);
         }
 
diff --git a/src/main.rs b/src/main.rs
index 671baec..0cfee52 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,6 +8,7 @@ use std::collections::HashMap;
 use tracing::{debug, error, info, Level};
 
 mod account_manager;
+mod db;
 mod error;
 mod keystorage;
 mod mail_event;
@@ -84,6 +85,7 @@ pub struct Hoot {
     relays: relay::RelayPool,
     events: Vec<nostr::Event>,
     account_manager: account_manager::AccountManager,
+    db: db::Db,
 }
 
 #[derive(Debug, PartialEq)]
@@ -113,7 +115,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 +166,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_mail_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 +211,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 +229,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 +244,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 +294,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 +332,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 +368,24 @@ 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)
-                            .and_then(|s| s.content())
-                            .map(|c| c.to_string())
-                            .unwrap_or_else(|| "No Subject".to_string()));
-                        
+                        ui.heading(
+                            &unwrapped
+                                .rumor
+                                .tags
+                                .find(nostr::TagKind::Subject)
+                                .and_then(|s| s.content())
+                                .map(|c| c.to_string())
+                                .unwrap_or_else(|| "No Subject".to_string()),
+                        );
+
                         // Metadata grid
                         egui::Grid::new("email_metadata")
                             .num_columns(2)
@@ -341,11 +396,16 @@ 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(
+                                    unwrapped
+                                        .rumor
+                                        .tags
+                                        .iter()
+                                        .filter_map(|tag| tag.content())
+                                        .next()
+                                        .unwrap_or_else(|| "Unknown")
+                                        .to_string(),
+                                );
                                 ui.end_row();
                             });
 
@@ -387,8 +447,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 +455,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 +482,7 @@ impl Hoot {
             relays: relay::RelayPool::new(),
             events: Vec::new(),
             account_manager: account_manager::AccountManager::new(),
+            db,
         }
     }
 }

From 9814f9277a40dbdc622cef9e50756ab8bf7c0950 Mon Sep 17 00:00:00 2001
From: Jack Chakany <jack@chakany.systems>
Date: Wed, 12 Mar 2025 11:15:23 -0400
Subject: [PATCH 3/5] change all cols in events to virtual ones that are
 generated on read, additionally refactor the insert event function

---
 migrations/001-nostr_events/up.sql | 17 +++++---
 src/db.rs                          | 66 ++++++------------------------
 src/main.rs                        |  2 +-
 3 files changed, 24 insertions(+), 61 deletions(-)

diff --git a/migrations/001-nostr_events/up.sql b/migrations/001-nostr_events/up.sql
index d1852ec..bb45ba2 100644
--- a/migrations/001-nostr_events/up.sql
+++ b/migrations/001-nostr_events/up.sql
@@ -1,9 +1,14 @@
 CREATE TABLE IF NOT EXISTS events (
     id TEXT PRIMARY KEY,
-    pubkey TEXT NOT NULL,
-    created_at INTEGER NOT NULL,
-    kind INTEGER NOT NULL,
-    tags TEXT NOT NULL CHECK (json_valid (tags)),
-    content TEXT NOT NULL,
-    sig TEXT
+    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);
diff --git a/src/db.rs b/src/db.rs
index dc47ca5..420f397 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -30,11 +30,7 @@ impl Db {
         Ok(Self { connection: conn })
     }
 
-    /// Store a mail event in the database
-    ///
-    /// This function first attempts to unwrap the gift wrap if necessary,
-    /// and then stores the event in the database.
-    pub fn store_mail_event(
+    pub fn store_event(
         &self,
         event: &Event,
         account_manager: &mut AccountManager,
@@ -43,53 +39,33 @@ impl Db {
         let store_unwrapped =
             is_gift_wrap(event) && account_manager.unwrap_gift_wrap(event).is_ok();
 
-        // Determine what event to store
         if store_unwrapped {
-            // Unwrap succeeded, store the unwrapped event
             let unwrapped = account_manager.unwrap_gift_wrap(event).unwrap();
+            let mut rumor = unwrapped.rumor.clone();
+            rumor.ensure_id();
 
-            // Get event details from the unwrapped gift
-            let id = match unwrapped.rumor.id {
-                Some(id) => id.to_string(),
-                None => "unknown".to_string(),
-            };
-            let pubkey = unwrapped.rumor.pubkey.to_string();
-            let created_at = unwrapped.rumor.created_at.as_u64();
-            let kind = unwrapped.rumor.kind.as_u16() as u32;
-            let tags_json = json!(unwrapped.rumor.tags).to_string();
-            let content = unwrapped.rumor.content.clone();
-            let sig = unwrapped.sender.to_string(); // Use sender pubkey as signature reference
+            let id = rumor.id.expect("Invalid Gift Wrapped Event: There is no ID!").to_hex();
+            let raw = json!(rumor).to_string();
 
-            // Store the unwrapped event in the database
             self.connection.execute(
-                "INSERT OR REPLACE INTO events (id, pubkey, created_at, kind, tags, content, sig)
-                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
-                (id, pubkey, created_at, kind, tags_json, content, sig),
+                "INSERT INTO events (id, raw)
+                 VALUES (?1, ?2)",
+                (id, raw),
             )?;
         } else {
-            // Use original event
-            // Convert tags to JSON string for storage
-            let tags_json = json!(event.tags).to_string();
-
-            // Get event details
             let id = event.id.to_string();
-            let pubkey = event.pubkey.to_string();
-            let created_at = event.created_at.as_u64();
-            let kind = event.kind.as_u16() as u32;
-            let sig = event.sig.to_string();
+            let raw = json!(event).to_string();
 
-            // Store the event in the database
             self.connection.execute(
-                "INSERT OR REPLACE INTO events (id, pubkey, created_at, kind, tags, content, sig)
-                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
-                (id, pubkey, created_at, kind, tags_json, &event.content, sig),
+                "INSERT INTO events (id, raw)
+                 VALUES (?1, ?2)",
+                (id, raw),
             )?;
         }
 
         Ok(())
     }
 
-    /// Check if the database contains an event with the given ID
     pub fn has_event(&self, event_id: &str) -> Result<bool> {
         let count: i64 = self.connection.query_row(
             "SELECT COUNT(*) FROM events WHERE id = ?",
@@ -125,24 +101,6 @@ impl Db {
 
         Ok(ids)
     }
-
-    /// Get the JSON representation of an event by its ID
-    pub fn get_event_json(&self, event_id: &str) -> Result<Option<String>> {
-        let result = self.connection.query_row(
-            "SELECT json_object('id', id, 'pubkey', pubkey, 'created_at', created_at,
-                              'kind', kind, 'tags', json(tags), 'content', content,
-                              'sig', sig)
-             FROM events WHERE id = ?",
-            [event_id],
-            |row| row.get::<_, String>(0),
-        );
-
-        match result {
-            Ok(json) => Ok(Some(json)),
-            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
-            Err(e) => Err(e.into()),
-        }
-    }
 }
 
 /// Check if an event is a gift wrap
diff --git a/src/main.rs b/src/main.rs
index 0cfee52..ba95caa 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -182,7 +182,7 @@ fn process_event(app: &mut Hoot, _sub_id: &str, event_json: &str) {
             app.events.push(event.clone());
 
             // Store the event in the database
-            if let Err(e) = app.db.store_mail_event(&event, &mut app.account_manager) {
+            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);

From 837c96e3469657174dc80df900b7225a5b666a54 Mon Sep 17 00:00:00 2001
From: Jack Chakany <jack@chakany.systems>
Date: Fri, 14 Mar 2025 09:42:34 -0400
Subject: [PATCH 4/5] make compose window less stupid

---
 src/ui/compose_window.rs | 71 ----------------------------------------
 1 file changed, 71 deletions(-)

diff --git a/src/ui/compose_window.rs b/src/ui/compose_window.rs
index 9bcc4b9..5fd9a76 100644
--- a/src/ui/compose_window.rs
+++ b/src/ui/compose_window.rs
@@ -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

From 0d8afd7340d46e0c2c7b4a65166548e75b439b84 Mon Sep 17 00:00:00 2001
From: Jack Chakany <jack@chakany.systems>
Date: Fri, 14 Mar 2025 10:10:18 -0400
Subject: [PATCH 5/5] make replies work

---
 src/main.rs | 42 ++++++++++++++++++++++--------------------
 1 file changed, 22 insertions(+), 20 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index ba95caa..8b26f43 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,6 +5,7 @@ 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;
@@ -374,17 +375,18 @@ fn render_app(app: &mut Hoot, ctx: &egui::Context) {
                     .find(|e| e.id.to_string() == app.focused_post)
                 {
                     if let Ok(unwrapped) = app.account_manager.unwrap_gift_wrap(event) {
+                        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());
                         // Message header section
                         ui.add_space(8.0);
-                        ui.heading(
-                            &unwrapped
-                                .rumor
-                                .tags
-                                .find(nostr::TagKind::Subject)
-                                .and_then(|s| s.content())
-                                .map(|c| c.to_string())
-                                .unwrap_or_else(|| "No Subject".to_string()),
-                        );
+                        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")
@@ -396,16 +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();
                             });
 
@@ -423,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