adding mail events into sqlite
This commit is contained in:
parent
21301cc603
commit
a2deb4145b
6 changed files with 378 additions and 108 deletions
72
Cargo.lock
generated
72
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"
|
||||
|
@ -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"
|
||||
|
|
22
Cargo.toml
22
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"
|
||||
|
|
9
migrations/001-nostr_events/up.sql
Normal file
9
migrations/001-nostr_events/up.sql
Normal file
|
@ -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
|
||||
);
|
151
src/db.rs
Normal file
151
src/db.rs
Normal file
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
212
src/main.rs
212
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue