run cargo fmt
Some checks failed
Rust CI / build-and-test (macOS-latest) (push) Has been cancelled
Rust CI / build-and-test (ubuntu-latest) (push) Has been cancelled
Rust CI / build-and-test (windows-latest) (push) Has been cancelled
Rust CI / cross-compile (macOS-latest, x86_64-pc-windows-gnu) (push) Has been cancelled
Rust CI / cross-compile (macOS-latest, x86_64-unknown-linux-gnu) (push) Has been cancelled
Rust CI / cross-compile (ubuntu-latest, x86_64-pc-windows-gnu) (push) Has been cancelled

This commit is contained in:
Jack Chakany 2025-03-20 10:06:16 -04:00
parent a68e5d5324
commit 1938d68888
7 changed files with 156 additions and 135 deletions

View file

@ -1,6 +1,6 @@
use crate::keystorage::{Error, Result, KeyStorage, KeyStorageType}; use crate::keystorage::{Error, KeyStorage, KeyStorageType, Result};
use nostr::{Keys, Event};
use nostr::nips::nip59::UnwrappedGift; use nostr::nips::nip59::UnwrappedGift;
use nostr::{Event, Keys};
use pollster::FutureExt as _; use pollster::FutureExt as _;
pub struct AccountManager { pub struct AccountManager {
@ -15,12 +15,16 @@ impl AccountManager {
} }
pub fn unwrap_gift_wrap(&mut self, gift_wrap: &Event) -> Result<UnwrappedGift> { pub fn unwrap_gift_wrap(&mut self, gift_wrap: &Event) -> Result<UnwrappedGift> {
let target_pubkey = gift_wrap.tags.iter() let target_pubkey = gift_wrap
.tags
.iter()
.find(|tag| tag.kind() == "p".into()) .find(|tag| tag.kind() == "p".into())
.and_then(|tag| tag.content()) .and_then(|tag| tag.content())
.ok_or(Error::KeyNotFound)?; .ok_or(Error::KeyNotFound)?;
let target_key = self.loaded_keys.iter() let target_key = self
.loaded_keys
.iter()
.find(|key| key.public_key().to_string() == *target_pubkey) .find(|key| key.public_key().to_string() == *target_pubkey)
.ok_or(Error::KeyNotFound)?; .ok_or(Error::KeyNotFound)?;

View file

@ -1,4 +1,4 @@
use nostr::{Event, EventBuilder, Keys, Kind, PublicKey, Tag, TagKind, TagStandard, EventId}; use nostr::{Event, EventBuilder, EventId, Keys, Kind, PublicKey, Tag, TagKind, TagStandard};
use pollster::FutureExt as _; use pollster::FutureExt as _;
use std::collections::HashMap; use std::collections::HashMap;
@ -31,7 +31,7 @@ impl MailMessage {
)); ));
pubkeys_to_send_to.push(*pubkey); pubkeys_to_send_to.push(*pubkey);
} }
for event in &self.parent_events { for event in &self.parent_events {
tags.push(Tag::event(*event)); tags.push(Tag::event(*event));
} }

View file

@ -1,11 +1,11 @@
use ewebsock::{WsMessage, WsEvent}; use crate::error;
use ewebsock::{WsEvent, WsMessage};
use nostr::types::Filter; use nostr::types::Filter;
use nostr::Event; use nostr::Event;
use serde::de::{SeqAccess, Visitor}; use serde::de::{SeqAccess, Visitor};
use serde::ser::SerializeSeq; use serde::ser::SerializeSeq;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt::{self}; use std::fmt::{self};
use crate::error;
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
pub struct CommandResult<'a> { pub struct CommandResult<'a> {
@ -29,7 +29,7 @@ pub enum RelayEvent<'a> {
Closed, Closed,
Other(&'a WsMessage), Other(&'a WsMessage),
Error(error::Error), Error(error::Error),
Message(RelayMessage<'a>) Message(RelayMessage<'a>),
} }
impl<'a> From<&'a WsEvent> for RelayEvent<'a> { impl<'a> From<&'a WsEvent> for RelayEvent<'a> {
@ -104,17 +104,17 @@ impl<'a> RelayMessage<'a> {
if let Some(comma_index) = msg[start..].find(',') { if let Some(comma_index) = msg[start..].find(',') {
let subid_end = start + comma_index; let subid_end = start + comma_index;
let subid = &msg[start..subid_end].trim().trim_matches('"'); let subid = &msg[start..subid_end].trim().trim_matches('"');
// Find start of event JSON after subscription ID // Find start of event JSON after subscription ID
let event_start = subid_end + 1; let event_start = subid_end + 1;
let mut event_start = event_start; let mut event_start = event_start;
while let Some(&b' ') = msg.as_bytes().get(event_start) { while let Some(&b' ') = msg.as_bytes().get(event_start) {
event_start += 1; event_start += 1;
} }
// Event JSON goes until end, minus closing bracket // Event JSON goes until end, minus closing bracket
let event_json = &msg[event_start..msg.len()-1]; let event_json = &msg[event_start..msg.len() - 1];
return Ok(Self::event(event_json, subid)); return Ok(Self::event(event_json, subid));
} else { } else {
return Ok(Self::event("{}", "fixme")); // Empty event JSON if parsing fails return Ok(Self::event("{}", "fixme")); // Empty event JSON if parsing fails

View file

@ -4,8 +4,8 @@ use crate::relay::Subscription;
use crate::relay::{Relay, RelayStatus}; use crate::relay::{Relay, RelayStatus};
use ewebsock::{WsEvent, WsMessage}; use ewebsock::{WsEvent, WsMessage};
use std::collections::HashMap; use std::collections::HashMap;
use tracing::{error, debug}; use std::time::{Duration, Instant};
use std::time::{Instant, Duration}; use tracing::{debug, error};
pub const RELAY_RECONNECT_SECONDS: u64 = 5; pub const RELAY_RECONNECT_SECONDS: u64 = 5;
@ -34,7 +34,9 @@ impl RelayPool {
let now = Instant::now(); let now = Instant::now();
// Check disconnected relays // Check disconnected relays
if now.duration_since(self.last_reconnect_attempt) >= Duration::from_secs(RELAY_RECONNECT_SECONDS) { if now.duration_since(self.last_reconnect_attempt)
>= Duration::from_secs(RELAY_RECONNECT_SECONDS)
{
for relay in self.relays.values_mut() { for relay in self.relays.values_mut() {
if relay.status != RelayStatus::Connected { if relay.status != RelayStatus::Connected {
relay.status = RelayStatus::Connecting; relay.status = RelayStatus::Connecting;
@ -145,7 +147,11 @@ impl RelayPool {
} }
} }
Pong(m) => { Pong(m) => {
debug!("pong recieved from {} after approx {} seconds", &url, self.last_ping.elapsed().as_secs()); debug!(
"pong recieved from {} after approx {} seconds",
&url,
self.last_ping.elapsed().as_secs()
);
} }
_ => { _ => {
// who cares // who cares

View file

@ -21,7 +21,7 @@ impl ComposeWindow {
let screen_rect = ctx.screen_rect(); let screen_rect = ctx.screen_rect();
let min_width = screen_rect.width().min(600.0); let min_width = screen_rect.width().min(600.0);
let min_height = screen_rect.height().min(400.0); let min_height = screen_rect.height().min(400.0);
// First collect all window IDs and their minimized state // First collect all window IDs and their minimized state
let state = app let state = app
.state .state
@ -29,131 +29,136 @@ impl ComposeWindow {
.get_mut(&id) .get_mut(&id)
.expect("no state found for id"); .expect("no state found for id");
egui::Window::new("New Message") egui::Window::new("New Message")
.id(id) .id(id)
.default_size([min_width, min_height]) .default_size([min_width, min_height])
.min_width(300.0) .min_width(300.0)
.min_height(200.0) .min_height(200.0)
.default_pos([screen_rect.right() - min_width - 20.0, screen_rect.bottom() - min_height - 20.0]) .default_pos([
.show(ctx, |ui| { screen_rect.right() - min_width - 20.0,
ui.vertical(|ui| { screen_rect.bottom() - min_height - 20.0,
// Header section ])
ui.horizontal(|ui| { .show(ctx, |ui| {
ui.label("To:"); ui.vertical(|ui| {
// Header section
ui.horizontal(|ui| {
ui.label("To:");
ui.add_sized(
[ui.available_width(), 24.0],
egui::TextEdit::singleline(&mut state.to_field),
);
});
ui.horizontal(|ui| {
ui.label("Subject:");
ui.add_sized(
[ui.available_width(), 24.0],
egui::TextEdit::singleline(&mut state.subject),
);
});
// Toolbar
ui.horizontal(|ui| {
ui.style_mut().spacing.button_padding = egui::vec2(4.0, 4.0);
if ui.button("B").clicked() {}
if ui.button("I").clicked() {}
if ui.button("U").clicked() {}
ui.separator();
if ui.button("🔗").clicked() {}
if ui.button("📎").clicked() {}
if ui.button("😀").clicked() {}
ui.separator();
if ui.button("").clicked() {}
});
// Message content
let available_height = ui.available_height() - 40.0; // Reserve space for bottom bar
egui::ScrollArea::vertical()
.max_height(available_height)
.show(ui, |ui| {
ui.add_sized( ui.add_sized(
[ui.available_width(), 24.0], [ui.available_width(), available_height - 20.0],
egui::TextEdit::singleline(&mut state.to_field) egui::TextEdit::multiline(&mut state.content),
); );
}); });
ui.horizontal(|ui| { // Bottom bar with account selector and send button
ui.label("Subject:"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.add_sized( if ui.button("Send").clicked() {
[ui.available_width(), 24.0], if state.selected_account.is_none() {
egui::TextEdit::singleline(&mut state.subject) error!("No Account Selected!");
); return;
}); }
// convert to field into PublicKey object
let to_field = state.to_field.clone();
// Toolbar let mut recipient_keys: Vec<PublicKey> = Vec::new();
ui.horizontal(|ui| { for key_string in to_field.split_whitespace() {
ui.style_mut().spacing.button_padding = egui::vec2(4.0, 4.0); use nostr::FromBech32;
if ui.button("B").clicked() {} match PublicKey::from_bech32(key_string) {
if ui.button("I").clicked() {} Ok(k) => recipient_keys.push(k),
if ui.button("U").clicked() {} Err(e) => debug!("could not parse public key as bech32: {}", e),
ui.separator();
if ui.button("🔗").clicked() {}
if ui.button("📎").clicked() {}
if ui.button("😀").clicked() {}
ui.separator();
if ui.button("").clicked() {}
});
// Message content
let available_height = ui.available_height() - 40.0; // Reserve space for bottom bar
egui::ScrollArea::vertical()
.max_height(available_height)
.show(ui, |ui| {
ui.add_sized(
[ui.available_width(), available_height - 20.0],
egui::TextEdit::multiline(&mut state.content)
);
});
// Bottom bar with account selector and send button
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Send").clicked() {
if state.selected_account.is_none() {
error!("No Account Selected!");
return;
}
// convert to field into PublicKey object
let to_field = state.to_field.clone();
let mut recipient_keys: Vec<PublicKey> = Vec::new();
for key_string in to_field.split_whitespace() {
use nostr::FromBech32;
match PublicKey::from_bech32(key_string) {
Ok(k) => recipient_keys.push(k),
Err(e) => debug!("could not parse public key as bech32: {}", e),
};
match PublicKey::from_hex(key_string) {
Ok(k) => recipient_keys.push(k),
Err(e) => debug!("could not parse public key as hex: {}", e),
};
}
let mut msg = MailMessage {
to: recipient_keys,
cc: vec![],
bcc: vec![],
parent_events: state.parent_events.clone(),
subject: state.subject.clone(),
content: state.content.clone(),
}; };
let events_to_send =
msg.to_events(&state.selected_account.clone().unwrap());
// send over wire match PublicKey::from_hex(key_string) {
for event in events_to_send { Ok(k) => recipient_keys.push(k),
match serde_json::to_string(&ClientMessage::Event { event: event.1 }) { Err(e) => debug!("could not parse public key as hex: {}", e),
Ok(v) => match app.relays.send(ewebsock::WsMessage::Text(v)) { };
Ok(r) => r, }
Err(e) => error!("could not send event to relays: {}", e),
}, let mut msg = MailMessage {
Err(e) => error!("could not serialize event: {}", e), to: recipient_keys,
}; cc: vec![],
bcc: vec![],
parent_events: state.parent_events.clone(),
subject: state.subject.clone(),
content: state.content.clone(),
};
let events_to_send =
msg.to_events(&state.selected_account.clone().unwrap());
// send over wire
for event in events_to_send {
match serde_json::to_string(&ClientMessage::Event {
event: event.1,
}) {
Ok(v) => match app.relays.send(ewebsock::WsMessage::Text(v)) {
Ok(r) => r,
Err(e) => error!("could not send event to relays: {}", e),
},
Err(e) => error!("could not serialize event: {}", e),
};
}
}
// Account selector
let accounts = app.account_manager.loaded_keys.clone();
use nostr::ToBech32;
let mut formatted_key = String::new();
if state.selected_account.is_some() {
formatted_key = state
.selected_account
.clone()
.unwrap()
.public_key()
.to_bech32()
.unwrap();
}
egui::ComboBox::from_id_source("account_selector")
.selected_text(format!("{}", formatted_key))
.show_ui(ui, |ui| {
for key in accounts {
ui.selectable_value(
&mut state.selected_account,
Some(key.clone()),
key.public_key().to_bech32().unwrap(),
);
} }
} });
// Account selector
let accounts = app.account_manager.loaded_keys.clone();
use nostr::ToBech32;
let mut formatted_key = String::new();
if state.selected_account.is_some() {
formatted_key = state
.selected_account
.clone()
.unwrap()
.public_key()
.to_bech32()
.unwrap();
}
egui::ComboBox::from_id_source("account_selector")
.selected_text(format!("{}", formatted_key))
.show_ui(ui, |ui| {
for key in accounts {
ui.selectable_value(
&mut state.selected_account,
Some(key.clone()),
key.public_key().to_bech32().unwrap(),
);
}
});
});
}); });
}); });
});
} }
// Keep the original show method for backward compatibility // Keep the original show method for backward compatibility

View file

@ -51,7 +51,12 @@ impl OnboardingScreen {
// here, we are assuming that the most recent key added is the one that was generated in // here, we are assuming that the most recent key added is the one that was generated in
// onboarding_new()'s button click. // onboarding_new()'s button click.
let first_key = app.account_manager.loaded_keys.last().expect("wanted a key from last screen").clone(); let first_key = app
.account_manager
.loaded_keys
.last()
.expect("wanted a key from last screen")
.clone();
ui.label(format!( ui.label(format!(
"New identity: {}", "New identity: {}",
first_key.public_key().to_bech32().unwrap() first_key.public_key().to_bech32().unwrap()

View file

@ -107,7 +107,8 @@ impl SettingsScreen {
// TODO: this only updates when next frame is rendered, which can be more than // TODO: this only updates when next frame is rendered, which can be more than
// a few seconds between renders. Make it so it updates every second. // a few seconds between renders. Make it so it updates every second.
if relay.status == crate::relay::RelayStatus::Disconnected { if relay.status == crate::relay::RelayStatus::Disconnected {
let next_ping = crate::relay::RELAY_RECONNECT_SECONDS - last_ping.elapsed().as_secs(); let next_ping =
crate::relay::RELAY_RECONNECT_SECONDS - last_ping.elapsed().as_secs();
ui.label(format!("(Attempting reconnect in {} seconds)", next_ping)); ui.label(format!("(Attempting reconnect in {} seconds)", next_ping));
} }