notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 48af3dde9d898a7b9fff70c9b166ac38b82c7c44
parent e629402d11f2d60281a36eaa051d6eb4ddae612b
Author: William Casarin <jb55@jb55.com>
Date:   Mon, 12 Dec 2022 14:33:37 -0800

many improvements

Diffstat:
M.gitignore | 1+
MCargo.lock | 2++
MCargo.toml | 3++-
Menostr/src/event.rs | 27+++++++++++++++++++++------
Menostr/src/filter.rs | 17+++++++++--------
Menostr/src/lib.rs | 7++++++-
Aenostr/src/profile.rs | 38++++++++++++++++++++++++++++++++++++++
Aenostr/src/pubkey.rs | 22++++++++++++++++++++++
Menostr/src/relay/mod.rs | 9++++++---
Menostr/src/relay/pool.rs | 38++++++++++++++++++++++++++++++++------
Msrc/app.rs | 265+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/bin/main.rs | 1-
Asrc/contacts.rs | 16++++++++++++++++
Msrc/error.rs | 11++++++++---
Msrc/lib.rs | 1+
15 files changed, 337 insertions(+), 121 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -7,3 +7,4 @@ target src/camera.rs *.patch *.txt +/tags diff --git a/Cargo.lock b/Cargo.lock @@ -651,10 +651,12 @@ dependencies = [ "poll-promise", "serde", "serde_derive", + "serde_json", "tokio", "tracing", "tracing-subscriber", "tracing-wasm", + "wasm-bindgen-futures", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml @@ -24,14 +24,15 @@ serde_derive = "1" serde = { version = "1", features = ["derive"] } # You only need this if you want app persistence tracing = "0.1.37" #wasm-bindgen = "0.2.83" -#wasm-bindgen-futures = "0.4" enostr = { path = "enostr" } +serde_json = "1.0.89" # web: [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1.6" tracing-wasm = "0.2" +wasm-bindgen-futures = "0.4" # native: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/enostr/src/event.rs b/enostr/src/event.rs @@ -1,4 +1,4 @@ -use crate::{Error, Result}; +use crate::{Error, Pubkey, Result}; use serde_derive::{Deserialize, Serialize}; use std::hash::{Hash, Hasher}; @@ -6,10 +6,10 @@ use std::hash::{Hash, Hasher}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Event { /// 32-bytes sha256 of the the serialized event data - pub id: String, + pub id: EventId, /// 32-bytes hex-encoded public key of the event creator #[serde(rename = "pubkey")] - pub pubkey: String, + pub pubkey: Pubkey, /// unix timestamp in seconds pub created_at: u64, /// integer @@ -26,7 +26,7 @@ pub struct Event { // Implement Hash trait impl Hash for Event { fn hash<H: Hasher>(&self, state: &mut H) { - self.id.hash(state); + self.id.0.hash(state); } } @@ -59,8 +59,8 @@ impl Event { sig: &str, ) -> Result<Self> { let event = Event { - id: id.to_string(), - pubkey: pubkey.to_string(), + id: id.to_string().into(), + pubkey: pubkey.to_string().into(), created_at, kind, tags, @@ -79,3 +79,18 @@ impl std::str::FromStr for Event { Event::from_json(s) } } + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Hash)] +pub struct EventId(String); + +impl From<String> for EventId { + fn from(s: String) -> Self { + EventId(s) + } +} + +impl From<EventId> for String { + fn from(evid: EventId) -> Self { + evid.0 + } +} diff --git a/enostr/src/filter.rs b/enostr/src/filter.rs @@ -1,19 +1,20 @@ +use crate::{EventId, Pubkey}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct Filter { #[serde(skip_serializing_if = "Option::is_none")] - ids: Option<Vec<String>>, + ids: Option<Vec<EventId>>, #[serde(skip_serializing_if = "Option::is_none")] - authors: Option<Vec<String>>, + authors: Option<Vec<Pubkey>>, #[serde(skip_serializing_if = "Option::is_none")] kinds: Option<Vec<u64>>, #[serde(rename = "#e")] #[serde(skip_serializing_if = "Option::is_none")] - events: Option<Vec<String>>, + events: Option<Vec<EventId>>, #[serde(rename = "#p")] #[serde(skip_serializing_if = "Option::is_none")] - pubkeys: Option<Vec<String>>, + pubkeys: Option<Vec<Pubkey>>, #[serde(skip_serializing_if = "Option::is_none")] since: Option<u64>, // unix timestamp seconds #[serde(skip_serializing_if = "Option::is_none")] @@ -36,12 +37,12 @@ impl Filter { } } - pub fn ids(mut self, ids: Vec<String>) -> Self { + pub fn ids(mut self, ids: Vec<EventId>) -> Self { self.ids = Some(ids); self } - pub fn authors(mut self, authors: Vec<String>) -> Self { + pub fn authors(mut self, authors: Vec<Pubkey>) -> Self { self.authors = Some(authors); self } @@ -51,12 +52,12 @@ impl Filter { self } - pub fn events(mut self, events: Vec<String>) -> Self { + pub fn events(mut self, events: Vec<EventId>) -> Self { self.events = Some(events); self } - pub fn pubkeys(mut self, pubkeys: Vec<String>) -> Self { + pub fn pubkeys(mut self, pubkeys: Vec<Pubkey>) -> Self { self.pubkeys = Some(pubkeys); self } diff --git a/enostr/src/lib.rs b/enostr/src/lib.rs @@ -2,12 +2,17 @@ mod client; mod error; mod event; mod filter; +mod profile; +mod pubkey; mod relay; pub use client::ClientMessage; pub use error::Error; -pub use event::Event; +pub use event::{Event, EventId}; +pub use ewebsock; pub use filter::Filter; +pub use profile::Profile; +pub use pubkey::Pubkey; pub use relay::message::{RelayEvent, RelayMessage}; pub use relay::pool::{PoolEvent, RelayPool}; pub use relay::Relay; diff --git a/enostr/src/profile.rs b/enostr/src/profile.rs @@ -0,0 +1,38 @@ +use serde_json::Value; + +#[derive(Debug, Clone)] +pub struct Profile(Value); + +impl Profile { + pub fn new(value: Value) -> Profile { + Profile(value) + } + + pub fn name(&self) -> Option<&str> { + return self.0["name"].as_str(); + } + + pub fn display_name(&self) -> Option<&str> { + return self.0["display_name"].as_str(); + } + + pub fn lud06(&self) -> Option<&str> { + return self.0["lud06"].as_str(); + } + + pub fn lud16(&self) -> Option<&str> { + return self.0["lud16"].as_str(); + } + + pub fn about(&self) -> Option<&str> { + return self.0["about"].as_str(); + } + + pub fn picture(&self) -> Option<&str> { + return self.0["picture"].as_str(); + } + + pub fn website(&self) -> Option<&str> { + return self.0["website"].as_str(); + } +} diff --git a/enostr/src/pubkey.rs b/enostr/src/pubkey.rs @@ -0,0 +1,22 @@ +use serde_derive::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Hash)] +pub struct Pubkey(String); + +impl AsRef<str> for Pubkey { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl From<String> for Pubkey { + fn from(s: String) -> Self { + Pubkey(s) + } +} + +impl From<Pubkey> for String { + fn from(pk: Pubkey) -> Self { + pk.0 + } +} diff --git a/enostr/src/relay/mod.rs b/enostr/src/relay/mod.rs @@ -58,9 +58,12 @@ impl Relay { }) } - pub fn subscribe(&mut self, subid: String, filters: Vec<Filter>) { - let cmd = ClientMessage::req(subid, filters); - let txt = WsMessage::Text(cmd.to_json()); + pub fn send(&mut self, msg: &ClientMessage) { + let txt = WsMessage::Text(msg.to_json()); self.sender.send(txt); } + + pub fn subscribe(&mut self, subid: String, filters: Vec<Filter>) { + self.send(&ClientMessage::req(subid, filters)); + } } diff --git a/enostr/src/relay/pool.rs b/enostr/src/relay/pool.rs @@ -1,7 +1,8 @@ use crate::relay::message::RelayEvent; use crate::relay::Relay; -use crate::Result; -use tracing::error; +use crate::{ClientMessage, Result}; +use ewebsock::WsMessage; +use tracing::{debug, error}; #[derive(Debug)] pub struct PoolEvent<'a> { @@ -34,6 +35,21 @@ impl RelayPool { return false; } + pub fn send(&mut self, cmd: &ClientMessage) { + for relay in &mut self.relays { + relay.send(cmd); + } + } + + pub fn send_to(&mut self, cmd: &ClientMessage, relay_url: &str) { + for relay in &mut self.relays { + if relay.url == relay_url { + relay.send(cmd); + return; + } + } + } + // Adds a websocket url to the RelayPool. pub fn add_url( &mut self, @@ -47,11 +63,23 @@ impl RelayPool { Ok(()) } - pub fn try_recv(&self) -> Option<PoolEvent<'_>> { - for relay in &self.relays { + /// Attempts to receive a pool event from a list of relays. The function searches each relay in the list in order, attempting to receive a message from each. If a message is received, return it. If no message is received from any relays, None is returned. + pub fn try_recv(&mut self) -> Option<PoolEvent<'_>> { + for relay in &mut self.relays { if let Some(msg) = relay.receiver.try_recv() { match msg.try_into() { Ok(event) => { + // let's just handle pongs here. + // We only need to do this natively. + #[cfg(not(target_arch = "wasm32"))] + match event { + RelayEvent::Other(WsMessage::Ping(ref bs)) => { + debug!("pong {}", &relay.url); + relay.sender.send(WsMessage::Pong(bs.to_owned())); + } + _ => {} + } + return Some(PoolEvent { event, relay: &relay.url, @@ -68,6 +96,4 @@ impl RelayPool { None } - - pub fn connect() {} } diff --git a/src/app.rs b/src/app.rs @@ -1,10 +1,12 @@ use egui_extras::RetainedImage; +use crate::contacts::Contacts; +use crate::Result; use egui::Context; -use enostr::{Filter, RelayEvent, RelayMessage}; +use enostr::{ClientMessage, EventId, Filter, Profile, Pubkey, RelayEvent, RelayMessage}; use poll_promise::Promise; -use std::collections::HashMap; -use std::hash::Hash; +use std::collections::{HashMap, HashSet}; +use std::hash::{Hash, Hasher}; use tracing::{debug, error, info, warn}; use enostr::{Event, RelayPool}; @@ -15,7 +17,15 @@ enum UrlKey<'a> { Failed(&'a str), } -type ImageCache<'a> = HashMap<UrlKey<'a>, Promise<ehttp::Result<RetainedImage>>>; +impl UrlKey<'_> { + fn to_u64(&self) -> u64 { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + self.hash(&mut hasher); + hasher.finish() + } +} + +type ImageCache = HashMap<u64, Promise<Result<RetainedImage>>>; #[derive(Eq, PartialEq, Clone)] pub enum DamusState { @@ -24,35 +34,28 @@ pub enum DamusState { } /// We derive Deserialize/Serialize so we can persist app state on shutdown. -pub struct Damus<'a> { - // Example stuff: - label: String, +pub struct Damus { state: DamusState, - composing: bool, + contacts: Contacts, n_panels: u32, pool: RelayPool, - all_events: HashMap<String, Event>, - events: Vec<String>, - - img_cache: ImageCache<'a>, + all_events: HashMap<EventId, Event>, + events: Vec<EventId>, - value: f32, + img_cache: ImageCache, } -impl Default for Damus<'_> { +impl Default for Damus { fn default() -> Self { Self { - // Example stuff: - label: "Hello World!".to_owned(), state: DamusState::Initializing, - composing: false, + contacts: Contacts::new(), all_events: HashMap::new(), pool: RelayPool::default(), events: vec![], img_cache: HashMap::new(), - value: 2.7, n_panels: 1, } } @@ -65,14 +68,17 @@ pub fn is_mobile(ctx: &egui::Context) -> bool { fn relay_setup(pool: &mut RelayPool, ctx: &egui::Context) { let ctx = ctx.clone(); - let wakeup = move || ctx.request_repaint(); + let wakeup = move || { + debug!("Woke up"); + ctx.request_repaint(); + }; if let Err(e) = pool.add_url("wss://relay.damus.io".to_string(), wakeup) { error!("{:?}", e) } } fn send_initial_filters(pool: &mut RelayPool, relay_url: &str) { - let filter = Filter::new().limit(20).kinds(vec![1, 42]); + let filter = Filter::new().limit(100).kinds(vec![1, 42]); let subid = "initial"; for relay in &mut pool.relays { if relay.url == relay_url { @@ -92,8 +98,9 @@ fn try_process_event(damus: &mut Damus) { match ev.event { RelayEvent::Opened => send_initial_filters(&mut damus.pool, &relay), - RelayEvent::Closed => warn!("{} connection closed", &relay), /* TODO: handle reconnects */ - RelayEvent::Other(msg) => debug!("Other ws message: {:?}", msg), + // TODO: handle reconnects + RelayEvent::Closed => warn!("{} connection closed", &relay), + RelayEvent::Other(msg) => debug!("other event {:?}", &msg), RelayEvent::Message(msg) => process_message(damus, &relay, msg), } //info!("recv {:?}", ev) @@ -109,22 +116,90 @@ fn update_damus(damus: &mut Damus, ctx: &egui::Context) { try_process_event(damus); } -fn process_event(damus: &mut Damus, subid: &str, event: Event) { +fn process_metadata_event(damus: &mut Damus, ev: &Event) { + if let Some(prev_id) = damus.contacts.events.get(&ev.pubkey) { + if let Some(prev_ev) = damus.all_events.get(prev_id) { + // This profile event is older, ignore it + if prev_ev.created_at >= ev.created_at { + return; + } + } + } + + let profile: core::result::Result<serde_json::Value, serde_json::Error> = + serde_json::from_str(&ev.content); + + match profile { + Err(e) => { + debug!("Invalid profile data '{}': {:?}", &ev.content, &e); + } + Ok(v) if !v.is_object() => { + debug!("Invalid profile data: '{}'", &ev.content); + } + Ok(profile) => { + damus + .contacts + .events + .insert(ev.pubkey.clone(), ev.id.clone()); + + damus + .contacts + .profiles + .insert(ev.pubkey.clone(), Profile::new(profile)); + } + } +} + +fn process_event(damus: &mut Damus, _subid: &str, event: Event) { if damus.all_events.get(&event.id).is_some() { return; } + if event.kind == 0 { + process_metadata_event(damus, &event); + } + let cloned_id = event.id.clone(); damus.all_events.insert(cloned_id.clone(), event); damus.events.push(cloned_id); } +fn get_unknown_author_ids(damus: &Damus) -> Vec<Pubkey> { + let mut authors: HashSet<Pubkey> = HashSet::new(); + + for (_evid, ev) in damus.all_events.iter() { + if !damus.contacts.profiles.contains_key(&ev.pubkey) { + authors.insert(ev.pubkey.clone()); + } + } + + authors.into_iter().collect() +} + +fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) { + if subid == "initial" { + let authors = get_unknown_author_ids(damus); + let n_authors = authors.len(); + let filter = Filter::new().authors(authors).kinds(vec![0]); + info!( + "Getting {} unknown author profiles from {}", + n_authors, relay_url + ); + let msg = ClientMessage::req("profiles".to_string(), vec![filter]); + damus.pool.send_to(&msg, relay_url); + } else if subid == "profiles" { + info!("Got profiles from {}", relay_url); + let msg = ClientMessage::close("profiles".to_string()); + damus.pool.send_to(&msg, relay_url); + } +} + fn process_message(damus: &mut Damus, relay: &str, msg: RelayMessage) { match msg { RelayMessage::Event(subid, ev) => process_event(damus, &subid, ev), RelayMessage::Notice(msg) => warn!("Notice from {}: {}", relay, msg), RelayMessage::OK(cr) => info!("OK {:?}", cr), - RelayMessage::Eose(sid) => info!("EOSE {}", sid), + RelayMessage::Eose(sid) => handle_eose(damus, &sid, relay), } } @@ -136,7 +211,7 @@ fn render_damus(damus: &mut Damus, ctx: &Context) { } } -impl Damus<'_> { +impl Damus { pub fn add_test_events(&mut self) { add_test_events(self); } @@ -157,58 +232,63 @@ impl Damus<'_> { } #[allow(clippy::needless_pass_by_value)] -fn parse_response(response: ehttp::Response) -> Result<RetainedImage, String> { +fn parse_response(response: ehttp::Response) -> Result<RetainedImage> { let content_type = response.content_type().unwrap_or_default(); if content_type.starts_with("image/svg") { - RetainedImage::from_svg_bytes(&response.url, &response.bytes) + Ok(RetainedImage::from_svg_bytes( + &response.url, + &response.bytes, + )?) } else if content_type.starts_with("image/") { - RetainedImage::from_image_bytes(&response.url, &response.bytes) + Ok(RetainedImage::from_image_bytes( + &response.url, + &response.bytes, + )?) } else { - Err(format!( - "Expected image, found content-type {:?}", - content_type - )) + Err(format!("Expected image, found content-type {:?}", content_type).into()) } } -fn fetch_img(ctx: &egui::Context, url: &str) -> Promise<ehttp::Result<RetainedImage>> { +fn fetch_img(ctx: &egui::Context, url: &str) -> Promise<Result<RetainedImage>> { + // TODO: fetch image from local cache + fetch_img_from_net(ctx, url) +} + +fn fetch_img_from_net(ctx: &egui::Context, url: &str) -> Promise<Result<RetainedImage>> { let (sender, promise) = Promise::new(); let request = ehttp::Request::get(url); let ctx = ctx.clone(); ehttp::fetch(request, move |response| { - let image = response.and_then(parse_response); + let image = response.map_err(Into::into).and_then(parse_response); sender.send(image); // send the results back to the UI thread. ctx.request_repaint(); }); promise } -fn robohash(hash: &str) -> String { - return format!("https://robohash.org/{}", hash); -} - -fn render_pfp<'a>(ui: &mut egui::Ui, img_cache: &mut ImageCache<'a>, pk: &str, url: &'a str) { - let urlkey = UrlKey::Orig(url); +fn render_pfp(ui: &mut egui::Ui, img_cache: &mut ImageCache, url: &str) { + let urlkey = UrlKey::Orig(url).to_u64(); let m_cached_promise = img_cache.get(&urlkey); if m_cached_promise.is_none() { debug!("urlkey: {:?}", &urlkey); - img_cache.insert(UrlKey::Orig(url), fetch_img(ui.ctx(), &url)); + img_cache.insert(urlkey, fetch_img(ui.ctx(), url)); } let pfp_size = 50.0; + let no_pfp_url = "https://damus.io/img/no-profile.svg"; match img_cache[&urlkey].ready() { None => { ui.spinner(); // still loading } - Some(Err(err)) => { - error!("Initial image load failed: {}", err); - let failed_key = UrlKey::Failed(&url); + Some(Err(_err)) => { + let failed_key = UrlKey::Failed(url).to_u64(); let m_failed_promise = img_cache.get_mut(&failed_key); if m_failed_promise.is_none() { debug!("failed key: {:?}", &failed_key); - img_cache.insert(UrlKey::Failed(url), fetch_img(ui.ctx(), &robohash(pk))); + let no_pfp = fetch_img(ui.ctx(), no_pfp_url); + img_cache.insert(failed_key, no_pfp); } match img_cache[&failed_key].ready() { @@ -216,7 +296,7 @@ fn render_pfp<'a>(ui: &mut egui::Ui, img_cache: &mut ImageCache<'a>, pk: &str, u ui.spinner(); // still loading } Some(Err(e)) => { - error!("Image load error: {}", e); + error!("Image load error: {:?}", e); ui.label("❌"); } Some(Ok(img)) => { @@ -243,45 +323,47 @@ fn render_username(ui: &mut egui::Ui, pk: &str) { }); } -fn render_event(ui: &mut egui::Ui, img_cache: &mut ImageCache<'_>, ev: &Event) { - ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { - let damus_pic = "https://damus.io/img/damus.svg".into(); - //let damus_pic = "https://192.168.87.26/img/damus.svg".into(); - let jb55_pic = "https://cdn.jb55.com/img/red-me.jpg".into(); - //let jb55_pic = "http://192.168.87.26/img/red-me.jpg".into(); - let pic = if ev.pubkey == "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" - { - jb55_pic - } else { - damus_pic - }; - - render_pfp(ui, img_cache, &ev.pubkey, pic); - - ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { - render_username(ui, &ev.pubkey); - - ui.label(&ev.content); - }) - }); +fn render_events(ui: &mut egui::Ui, damus: &mut Damus) { + for evid in &damus.events { + if !damus.all_events.contains_key(evid) { + return; + } + + ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { + let ev = damus.all_events.get(evid).unwrap(); + + let m_pic = damus + .contacts + .profiles + .get(&ev.pubkey) + .and_then(|p| p.picture()); + + if let Some(pic) = m_pic { + render_pfp(ui, &mut damus.img_cache, pic); + } + + ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { + render_username(ui, ev.pubkey.as_ref()); + + ui.label(&ev.content); + }) + }); + + ui.separator(); + } } -fn timeline_view(ui: &mut egui::Ui, app: &mut Damus<'_>) { +fn timeline_view(ui: &mut egui::Ui, app: &mut Damus) { ui.heading("Timeline"); egui::ScrollArea::vertical() .auto_shrink([false; 2]) .show(ui, |ui| { - for evid in &app.events { - if let Some(ev) = app.all_events.get(evid) { - render_event(ui, &mut app.img_cache, ev); - ui.separator(); - } - } + render_events(ui, app); }); } -fn render_panel(ctx: &egui::Context, app: &mut Damus<'_>) { +fn render_panel(ctx: &egui::Context, app: &mut Damus) { egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { ui.horizontal_wrapped(|ui| { ui.visuals_mut().button_frame = false; @@ -295,27 +377,26 @@ fn render_panel(ctx: &egui::Context, app: &mut Damus<'_>) { app.n_panels += 1; } - if app.n_panels != 1 { - if ui + if app.n_panels != 1 + && ui .add(egui::Button::new("-").frame(false)) .on_hover_text("Remove Timeline") .clicked() - { - app.n_panels -= 1; - } + { + app.n_panels -= 1; } }); }); } -fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus<'_>) { +fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) { let panel_width = ctx.input().screen_rect.width(); egui::CentralPanel::default().show(ctx, |ui| { timeline_panel(ui, app, panel_width, 0); }); } -fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus<'_>) { +fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) { render_panel(ctx, app); let screen_size = ctx.input().screen_rect.width(); @@ -348,7 +429,7 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus<'_>) { }); } -fn timeline_panel(ui: &mut egui::Ui, app: &mut Damus<'_>, panel_width: f32, ind: u32) { +fn timeline_panel(ui: &mut egui::Ui, app: &mut Damus, panel_width: f32, ind: u32) { egui::SidePanel::left(format!("l{}", ind)) .resizable(false) .max_width(panel_width) @@ -358,15 +439,15 @@ fn timeline_panel(ui: &mut egui::Ui, app: &mut Damus<'_>, panel_width: f32, ind: }); } -fn add_test_events(damus: &mut Damus<'_>) { +fn add_test_events(damus: &mut Damus) { // Examples of how to create different panels and windows. // Pick whichever suits you. // Tip: a good default choice is to just keep the `CentralPanel`. // For inspiration and more examples, go to https://emilk.github.io/egui let test_event = Event { - id: "6938e3cd841f3111dbdbd909f87fd52c3d1f1e4a07fd121d1243196e532811cb".to_string(), - pubkey: "f0a6ff7f70b872de6d82c8daec692a433fd23b6a49f25923c6f034df715cdeec".to_string(), + id: "6938e3cd841f3111dbdbd909f87fd52c3d1f1e4a07fd121d1243196e532811cb".to_string().into(), + pubkey: "f0a6ff7f70b872de6d82c8daec692a433fd23b6a49f25923c6f034df715cdeec".to_string().into(), created_at: 1667781968, kind: 1, tags: vec![], @@ -375,8 +456,8 @@ fn add_test_events(damus: &mut Damus<'_>) { }; let test_event2 = Event { - id: "6938e3cd841f3111dbdbd909f87fd52c3d1f1e4a07fd121d1243196e532811cb".to_string(), - pubkey: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245".to_string(), + id: "6938e3cd841f3111dbdbd909f87fd52c3d1f1e4a07fd121d1243196e532811cb".to_string().into(), + pubkey: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245".to_string().into(), created_at: 1667781968, kind: 1, tags: vec![], @@ -391,9 +472,7 @@ fn add_test_events(damus: &mut Damus<'_>) { .all_events .insert(test_event2.id.clone(), test_event2.clone()); - if damus.events.len() == 0 { - damus.events.push(test_event.id.clone()); - damus.events.push(test_event2.id.clone()); + if damus.events.is_empty() { damus.events.push(test_event.id.clone()); damus.events.push(test_event2.id.clone()); damus.events.push(test_event.id.clone()); @@ -401,10 +480,12 @@ fn add_test_events(damus: &mut Damus<'_>) { damus.events.push(test_event.id.clone()); damus.events.push(test_event2.id.clone()); damus.events.push(test_event.id.clone()); + damus.events.push(test_event2.id); + damus.events.push(test_event.id); } } -impl eframe::App for Damus<'_> { +impl eframe::App for Damus { /// Called by the frame work to save state before shutdown. fn save(&mut self, _storage: &mut dyn eframe::Storage) { //eframe::set_value(storage, eframe::APP_KEY, self); diff --git a/src/bin/main.rs b/src/bin/main.rs @@ -1,7 +1,6 @@ #![warn(clippy::all, rust_2018_idioms)] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release use damus::Damus; -use eframe; // Entry point for wasm //#[cfg(target_arch = "wasm32")] diff --git a/src/contacts.rs b/src/contacts.rs @@ -0,0 +1,16 @@ +use enostr::{EventId, Profile, Pubkey}; +use std::collections::HashMap; + +pub struct Contacts { + pub events: HashMap<Pubkey, EventId>, + pub profiles: HashMap<Pubkey, Profile>, +} + +impl Contacts { + pub fn new() -> Contacts { + Contacts { + events: HashMap::new(), + profiles: HashMap::new(), + } + } +} diff --git a/src/error.rs b/src/error.rs @@ -1,8 +1,13 @@ -use enostr; - -#[derive(Eq, PartialEq)] +#[derive(Eq, PartialEq, Debug)] pub enum Error { Nostr(enostr::Error), + Generic(String), +} + +impl From<String> for Error { + fn from(s: String) -> Self { + Error::Generic(s) + } } impl From<enostr::Error> for Error { diff --git a/src/lib.rs b/src/lib.rs @@ -1,5 +1,6 @@ mod app; //mod camera; +mod contacts; mod error; pub use app::Damus;