From 7b0d9d6370c9577182d47e4f892bfb7126d299a9 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Thu, 29 Dec 2022 21:01:09 +1300 Subject: [PATCH] Replying has been implemented --- src/overlord/mod.rs | 63 ++++++++++++++++++- src/ui/feed.rs | 112 +++++++++++++++++++++++---------- src/ui/mod.rs | 4 +- src/ui/widgets/mod.rs | 3 + src/ui/widgets/reply_button.rs | 92 +++++++++++++++++++++++++++ 5 files changed, 240 insertions(+), 34 deletions(-) create mode 100644 src/ui/widgets/reply_button.rs diff --git a/src/overlord/mod.rs b/src/overlord/mod.rs index 3fb81948..4941bad5 100644 --- a/src/overlord/mod.rs +++ b/src/overlord/mod.rs @@ -8,7 +8,7 @@ use crate::globals::{Globals, GLOBALS}; use crate::settings::Settings; use minion::Minion; use nostr_types::{ - Event, EventKind, Id, Nip05, PreEvent, PrivateKey, PublicKey, PublicKeyHex, Unixtime, Url, + Event, EventKind, Id, Nip05, PreEvent, PrivateKey, PublicKey, PublicKeyHex, Tag, Unixtime, Url, }; use relay_picker::{BestRelay, RelayPicker}; use std::collections::HashMap; @@ -474,6 +474,11 @@ impl Overlord { let content: String = serde_json::from_str(&bus_message.json_payload)?; self.post_textnote(content).await?; } + "post_reply" => { + let (content, reply_to): (String, Id) = + serde_json::from_str(&bus_message.json_payload)?; + self.post_reply(content, reply_to).await?; + } _ => {} }, _ => {} @@ -704,4 +709,60 @@ impl Overlord { Ok(()) } + + async fn post_reply(&mut self, content: String, reply_to: Id) -> Result<(), Error> { + let event = { + let public_key = match GLOBALS.signer.read().await.public_key() { + Some(pk) => pk, + None => { + warn!("No public key! Not posting"); + return Ok(()); + } + }; + + let pre_event = PreEvent { + pubkey: public_key, + created_at: Unixtime::now().unwrap(), + kind: EventKind::TextNote, + tags: vec![Tag::Event { + id: reply_to, + recommended_relay_url: None, // FIXME - we should pick a URL shared by who I am replying to and myself + marker: Some("reply".to_string()), + }], + content, + ots: None, + }; + + GLOBALS.signer.read().await.sign_preevent(pre_event)? + }; + + let relays: Vec = GLOBALS + .relays + .read() + .await + .iter() + .filter_map(|(_, r)| if r.post { Some(r.to_owned()) } else { None }) + .collect(); + + for relay in relays { + // Start a minion for it, if there is none + if !self.urls_watching.contains(&Url::new(&relay.url)) { + self.start_minion(relay.url.clone()).await?; + } + + // Send it the event to post + debug!("Asking {} to post", &relay.url); + + let _ = self.to_minions.send(BusMessage { + target: relay.url.clone(), + kind: "post_event".to_string(), + json_payload: serde_json::to_string(&event).unwrap(), + }); + } + + // Process the message for ourself + crate::process::process_new_event(&event, false, None).await?; + + Ok(()) + } } diff --git a/src/ui/feed.rs b/src/ui/feed.rs index 536446c9..957a3e12 100644 --- a/src/ui/feed.rs +++ b/src/ui/feed.rs @@ -1,10 +1,11 @@ use super::{GossipUi, Page}; use crate::comms::BusMessage; use crate::globals::{Globals, GLOBALS}; -use crate::ui::widgets::CopyButton; +use crate::ui::widgets::{CopyButton, ReplyButton}; use eframe::egui; -use egui::{Align, Color32, Context, Layout, RichText, ScrollArea, Ui, Vec2}; +use egui::{Align, Color32, Context, Layout, RichText, ScrollArea, TextEdit, Ui, Vec2}; use nostr_types::{EventKind, Id}; +use tracing::debug; pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Frame, ui: &mut Ui) { let feed = GLOBALS.feed.blocking_lock().get(); @@ -16,49 +17,87 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram GLOBALS.desired_events.blocking_read().len() }; - ui.horizontal(|ui| { + ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { + ui.with_layout(Layout::top_down(Align::Max), |ui| { + if ui + .button(&format!("Get {} missing events", desired_count)) + .clicked() + { + let tx = GLOBALS.to_overlord.clone(); + let _ = tx.send(BusMessage { + target: "overlord".to_string(), + kind: "get_missing_events".to_string(), + json_payload: serde_json::to_string("").unwrap(), + }); + } + }); + }); + + ui.vertical(|ui| { if !GLOBALS.signer.blocking_read().is_ready() { ui.horizontal(|ui| { ui.label("You need to "); if ui.link("setup your identity").clicked() { app.page = Page::You; } + ui.label(" to post."); }); } else if !GLOBALS.relays.blocking_read().iter().any(|(_, r)| r.post) { ui.horizontal(|ui| { ui.label("You need to "); - if ui.link("choose relays to post to").clicked() { + if ui.link("choose relays").clicked() { app.page = Page::Relays; } + ui.label(" to post."); }); } else { - ui.text_edit_multiline(&mut app.draft); - - if ui.button("Send").clicked() && !app.draft.is_empty() { - let tx = GLOBALS.to_overlord.clone(); - let _ = tx.send(BusMessage { - target: "overlord".to_string(), - kind: "post_textnote".to_string(), - json_payload: serde_json::to_string(&app.draft).unwrap(), - }); - app.draft = "".to_owned(); + if let Some(id) = app.replying_to { + render_post(app, ctx, frame, ui, id, 0, true); } - } - ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { - ui.with_layout(Layout::top_down(Align::Max), |ui| { - if ui.button(&format!("Get {} missing events", desired_count)).clicked() { + ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { + if ui.button("Send").clicked() && !app.draft.is_empty() { let tx = GLOBALS.to_overlord.clone(); - let _ = tx.send(BusMessage { - target: "overlord".to_string(), - kind: "get_missing_events".to_string(), - json_payload: serde_json::to_string("").unwrap(), - }); + match app.replying_to { + Some(_id) => { + let _ = tx.send(BusMessage { + target: "overlord".to_string(), + kind: "post_reply".to_string(), + json_payload: serde_json::to_string(&( + &app.draft, + &app.replying_to, + )) + .unwrap(), + }); + } + None => { + let _ = tx.send(BusMessage { + target: "overlord".to_string(), + kind: "post_textnote".to_string(), + json_payload: serde_json::to_string(&app.draft).unwrap(), + }); + } + } + app.draft = "".to_owned(); + app.replying_to = None; } + if ui.button("Cancel").clicked() { + app.draft = "".to_owned(); + app.replying_to = None; + } + + ui.add( + TextEdit::multiline(&mut app.draft) + .hint_text("Type your message here") + .desired_width(f32::INFINITY) + .lock_focus(true), + ); }); - }); + } }); + ui.separator(); + ScrollArea::vertical().show(ui, |ui| { for id in feed.iter() { // Stop rendering at the bottom of the window: @@ -68,7 +107,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram // break; //} - render_post(app, ctx, frame, ui, *id, 0); + render_post(app, ctx, frame, ui, *id, 0, false); } }); } @@ -80,6 +119,7 @@ fn render_post( ui: &mut Ui, id: Id, indent: usize, + as_reply_to: bool, ) { let maybe_event = GLOBALS.events.blocking_read().get(&id).cloned(); if maybe_event.is_none() { @@ -203,19 +243,27 @@ fn render_post( ui.label(&event.content); // Under row - ui.horizontal(|ui| { - if ui.add(CopyButton {}).clicked() { - ui.output().copied_text = event.content.clone(); - } - }); + if !as_reply_to { + ui.horizontal(|ui| { + if ui.add(CopyButton {}).clicked() { + ui.output().copied_text = event.content.clone(); + } + + ui.add_space(24.0); + + if ui.add(ReplyButton {}).clicked() { + app.replying_to = Some(event.id); + } + }); + } }); }); ui.separator(); - if app.settings.view_threaded { + if app.settings.view_threaded && !as_reply_to { for reply_id in replies { - render_post(app, _ctx, _frame, ui, reply_id, indent + 1); + render_post(app, _ctx, _frame, ui, reply_id, indent + 1, as_reply_to); } } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 0e863fcb..c3722d69 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -14,7 +14,7 @@ use crate::globals::GLOBALS; use crate::settings::Settings; use eframe::{egui, IconData, Theme}; use egui::{ColorImage, Context, ImageData, TextureHandle, TextureOptions}; -use nostr_types::{PublicKey, PublicKeyHex}; +use nostr_types::{Id, PublicKey, PublicKeyHex}; use std::time::{Duration, Instant}; use zeroize::Zeroize; @@ -74,6 +74,7 @@ struct GossipUi { password: String, import_bech32: String, import_hex: String, + replying_to: Option, } impl Drop for GossipUi { @@ -139,6 +140,7 @@ impl GossipUi { password: "".to_owned(), import_bech32: "".to_owned(), import_hex: "".to_owned(), + replying_to: None, } } } diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs index 34985845..21099bf3 100644 --- a/src/ui/widgets/mod.rs +++ b/src/ui/widgets/mod.rs @@ -1,2 +1,5 @@ mod copy_button; pub use copy_button::CopyButton; + +mod reply_button; +pub use reply_button::ReplyButton; diff --git a/src/ui/widgets/reply_button.rs b/src/ui/widgets/reply_button.rs new file mode 100644 index 00000000..71abb16a --- /dev/null +++ b/src/ui/widgets/reply_button.rs @@ -0,0 +1,92 @@ +use eframe::{egui, epaint}; +use egui::{Color32, Pos2, Response, Sense, Shape, Ui, Vec2, Widget}; +use epaint::{PathShape, Stroke}; + +pub struct ReplyButton {} + +impl ReplyButton { + fn paint(ui: &mut Ui, corner: Pos2) { + ui.painter().add(Shape::Path(PathShape { + points: vec![ + Pos2 { + x: corner.x + 2.0, + y: corner.y + 0.0, + }, + Pos2 { + x: corner.x + 14.0, + y: corner.y + 0.0, + }, + Pos2 { + x: corner.x + 16.0, + y: corner.y + 2.0, + }, + Pos2 { + x: corner.x + 16.0, + y: corner.y + 8.0, + }, + Pos2 { + x: corner.x + 14.0, + y: corner.y + 10.0, + }, + Pos2 { + x: corner.x + 12.0, + y: corner.y + 10.0, + }, + Pos2 { + x: corner.x + 4.0, + y: corner.y + 15.0, + }, + Pos2 { + x: corner.x + 6.0, + y: corner.y + 10.0, + }, + Pos2 { + x: corner.x + 2.0, + y: corner.y + 10.0, + }, + Pos2 { + x: corner.x + 0.0, + y: corner.y + 8.0, + }, + Pos2 { + x: corner.x + 0.0, + y: corner.y + 2.0, + }, + Pos2 { + x: corner.x + 2.0, + y: corner.y + 0.0, + }, + ], + closed: true, + fill: Color32::TRANSPARENT, + stroke: Stroke { + width: 1.0, + color: Color32::from_rgb(0x8d, 0x7f, 0x73), + }, + })); + } +} + +impl Widget for ReplyButton { + fn ui(self, ui: &mut Ui) -> Response { + let padding = ui.spacing().button_padding; + let space = Vec2 { + x: 12.0 + padding.x * 2.0, + y: 12.0 + padding.y * 2.0, + }; + let (id, rect) = ui.allocate_space(space); + let response = ui.interact(rect, id, Sense::click()); + let shift = if response.is_pointer_button_down_on() { + 2.0 + } else { + 0.0 + }; + let pos = Pos2 { + x: rect.min.x + padding.x + shift, + y: rect.min.y + padding.y + shift, + }; + Self::paint(ui, ui.painter().round_pos_to_pixels(pos)); + + response + } +}