From f8bb4d74b05ea303fe9fec4e37afb62bba87a9fb Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 3 Jan 2023 10:03:06 +1300 Subject: [PATCH 1/8] UI rework and fixes, in preparation for three feeds --- src/ui/{feed.rs => feed/mod.rs} | 315 +++++++++++++++++--------------- src/ui/feed/person.rs | 7 + src/ui/feed/thread.rs | 7 + src/ui/help/about.rs | 71 +++++++ src/ui/{help.rs => help/mod.rs} | 88 ++------- src/ui/{ => help}/stats.rs | 0 src/ui/mod.rs | 97 +++++++--- src/ui/people.rs | 237 ------------------------ src/ui/people/follow.rs | 93 ++++++++++ src/ui/people/mod.rs | 101 ++++++++++ src/ui/people/person.rs | 75 ++++++++ 11 files changed, 613 insertions(+), 478 deletions(-) rename src/ui/{feed.rs => feed/mod.rs} (62%) create mode 100644 src/ui/feed/person.rs create mode 100644 src/ui/feed/thread.rs create mode 100644 src/ui/help/about.rs rename src/ui/{help.rs => help/mod.rs} (71%) rename src/ui/{ => help}/stats.rs (100%) delete mode 100644 src/ui/people.rs create mode 100644 src/ui/people/follow.rs create mode 100644 src/ui/people/mod.rs create mode 100644 src/ui/people/person.rs diff --git a/src/ui/feed.rs b/src/ui/feed/mod.rs similarity index 62% rename from src/ui/feed.rs rename to src/ui/feed/mod.rs index 55cc8ed4..6080adf4 100644 --- a/src/ui/feed.rs +++ b/src/ui/feed/mod.rs @@ -10,6 +10,9 @@ use egui::{ use linkify::{LinkFinder, LinkKind}; use nostr_types::{EventKind, Id, PublicKeyHex}; +mod person; +mod thread; + struct FeedPostParams { id: Id, indent: usize, @@ -18,161 +21,181 @@ struct FeedPostParams { } pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Frame, ui: &mut Ui) { - let feed = GLOBALS.feed.blocking_lock().get(); - - Globals::trim_desired_events_sync(); - let desired_count: isize = match GLOBALS.desired_events.try_read() { - Ok(v) => v.len() as isize, - Err(_) => -1, - }; - let incoming_count: isize = match GLOBALS.incoming_events.try_read() { - Ok(v) => v.len() as isize, - Err(_) => -1, - }; - - ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { - if ui - .button(&format!("QM {}", desired_count)) - .on_hover_text("Query Relays for Missing Events") - .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.horizontal(|ui| { + ui.selectable_value(&mut app.page, Page::FeedGeneral, "Following"); + ui.separator(); + if app.feed_thread_id.is_some() { + ui.selectable_value(&mut app.page, Page::FeedThread, "Thread"); + ui.separator(); } - - if ui - .button(&format!("PQ {}", incoming_count)) - .on_hover_text("Process Queue of Incoming Events") - .clicked() - { - let tx = GLOBALS.to_overlord.clone(); - let _ = tx.send(BusMessage { - target: "overlord".to_string(), - kind: "process_incoming_events".to_string(), - json_payload: serde_json::to_string("").unwrap(), - }); - } - - if ui.button("close all").clicked() { - app.hides = feed.clone(); - } - if ui.button("open all").clicked() { - app.hides.clear(); - } - ui.label(&format!( - "RIF={}", - GLOBALS - .fetcher - .requests_in_flight - .load(std::sync::atomic::Ordering::Relaxed) - )); - }); - - 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").clicked() { - app.page = Page::Relays; - } - ui.label(" to post."); - }); - } else { - if let Some(id) = app.replying_to { - render_post_actual( - app, - ctx, - frame, - ui, - FeedPostParams { - id, - indent: 0, - as_reply_to: true, - threaded: false, - }, - ); - } - - 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(); - 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), - ); - }); + if app.feed_person_pubkey.is_some() { + ui.selectable_value(&mut app.page, Page::FeedPerson, "Person"); + ui.separator(); } }); - ui.separator(); - let threaded = GLOBALS.settings.blocking_read().view_threaded; + if app.page == Page::FeedGeneral { + let feed = GLOBALS.feed.blocking_lock().get(); - ScrollArea::vertical().show(ui, |ui| { - let bgcolor = if ctx.style().visuals.dark_mode { - Color32::BLACK - } else { - Color32::WHITE + Globals::trim_desired_events_sync(); + let desired_count: isize = match GLOBALS.desired_events.try_read() { + Ok(v) => v.len() as isize, + Err(_) => -1, }; - Frame::none().fill(bgcolor).show(ui, |ui| { - for id in feed.iter() { - render_post_maybe_fake( - app, - ctx, - frame, - ui, - FeedPostParams { - id: *id, - indent: 0, - as_reply_to: false, - threaded, - }, - ); + let incoming_count: isize = match GLOBALS.incoming_events.try_read() { + Ok(v) => v.len() as isize, + Err(_) => -1, + }; + + ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { + if ui + .button(&format!("QM {}", desired_count)) + .on_hover_text("Query Relays for Missing Events") + .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(), + }); + } + + if ui + .button(&format!("PQ {}", incoming_count)) + .on_hover_text("Process Queue of Incoming Events") + .clicked() + { + let tx = GLOBALS.to_overlord.clone(); + let _ = tx.send(BusMessage { + target: "overlord".to_string(), + kind: "process_incoming_events".to_string(), + json_payload: serde_json::to_string("").unwrap(), + }); + } + + if ui.button("close all").clicked() { + app.hides = feed.clone(); + } + if ui.button("open all").clicked() { + app.hides.clear(); + } + ui.label(&format!( + "RIF={}", + GLOBALS + .fetcher + .requests_in_flight + .load(std::sync::atomic::Ordering::Relaxed) + )); + }); + + 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").clicked() { + app.page = Page::Relays; + } + ui.label(" to post."); + }); + } else { + if let Some(id) = app.replying_to { + render_post_actual( + app, + ctx, + frame, + ui, + FeedPostParams { + id, + indent: 0, + as_reply_to: true, + threaded: false, + }, + ); + } + + 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(); + 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(); + + let threaded = GLOBALS.settings.blocking_read().view_threaded; + + ScrollArea::vertical().show(ui, |ui| { + let bgcolor = if ctx.style().visuals.dark_mode { + Color32::BLACK + } else { + Color32::WHITE + }; + Frame::none().fill(bgcolor).show(ui, |ui| { + for id in feed.iter() { + render_post_maybe_fake( + app, + ctx, + frame, + ui, + FeedPostParams { + id: *id, + indent: 0, + as_reply_to: false, + threaded, + }, + ); + } + }); + }); + } else if app.page == Page::FeedThread { + thread::update(app, ctx, frame, ui); + } else if app.page == Page::FeedPerson { + person::update(app, ctx, frame, ui); + } } fn render_post_maybe_fake( diff --git a/src/ui/feed/person.rs b/src/ui/feed/person.rs new file mode 100644 index 00000000..f5c32dff --- /dev/null +++ b/src/ui/feed/person.rs @@ -0,0 +1,7 @@ +use super::GossipUi; +use eframe::egui; +use egui::{Context, Ui}; + +pub(super) fn update(_app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { + ui.label("Person TBD"); +} diff --git a/src/ui/feed/thread.rs b/src/ui/feed/thread.rs new file mode 100644 index 00000000..efc07ce2 --- /dev/null +++ b/src/ui/feed/thread.rs @@ -0,0 +1,7 @@ +use super::GossipUi; +use eframe::egui; +use egui::{Context, Ui}; + +pub(super) fn update(_app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { + ui.label("Thread TBD"); +} diff --git a/src/ui/help/about.rs b/src/ui/help/about.rs new file mode 100644 index 00000000..b5165c01 --- /dev/null +++ b/src/ui/help/about.rs @@ -0,0 +1,71 @@ +use super::GossipUi; +use eframe::egui; +use egui::{Align, Context, Layout, RichText, TextStyle, Ui}; + +pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { + ui.with_layout(Layout::top_down(Align::Center), |ui| { + + ui.add_space(30.0); + + ui.image(&app.icon, app.icon.size_vec2()); + + ui.add_space(15.0); + + ui.label( + RichText::new(&app.about.name).strong() + ); + + ui.add_space(15.0); + + ui.label( + RichText::new(&app.about.version) + .text_style(TextStyle::Body) + ); + + ui.add_space(15.0); + + ui.label( + RichText::new(&app.about.description) + .text_style(TextStyle::Body) + ); + + ui.add_space(35.0); + + ui.label( + RichText::new(format!("nostr is a protocol and specification for storing and retrieving social media events onto servers called relays. Many users store their events onto multiple relays for reliability, censorship resistance, and to spread their reach. If you didn't store an event on a particular relay, don't expect anyone to find it there because relays normally don't share events with each other. + +Users are defined by their keypair, and are known by the public key of that pair. All events they generate are signed by their private key, and verifiable by their public key. + +We are storing data on your system in this file: {}. This data is only used locally by this client - the nostr protocol does not use clients as a store of data for other people. We are storing your settings, your private and public key, information about relays, and a cache of events. We cache events in your feed so that we don't have to ask relays for them again, which means less network traffic and faster startup times. +", app.about.storage_path)) + .text_style(TextStyle::Body) + ); + + ui.add_space(22.0); + + ui.hyperlink_to("Learn More about Nostr", "https://github.com/nostr-protocol/nostr"); + + ui.add_space(30.0); + + ui.hyperlink_to("Source Code", &app.about.homepage); + ui.label( + RichText::new("by") + .text_style(TextStyle::Small) + ); + ui.label( + RichText::new(&app.about.authors) + .text_style(TextStyle::Small) + ); + + ui.add_space(15.0); + + ui.label( + RichText::new("This program comes with absolutely no warranty.") + .text_style(TextStyle::Small) + ); + ui.label( + RichText::new("See the MIT License for details.") + .text_style(TextStyle::Small) + ); + }); +} diff --git a/src/ui/help.rs b/src/ui/help/mod.rs similarity index 71% rename from src/ui/help.rs rename to src/ui/help/mod.rs index b1941995..0932ecb3 100644 --- a/src/ui/help.rs +++ b/src/ui/help/mod.rs @@ -1,16 +1,20 @@ use super::{GossipUi, Page}; use eframe::egui; -use egui::{Align, Context, Layout, RichText, ScrollArea, TextStyle, TopBottomPanel, Ui}; +use egui::{Context, ScrollArea, Ui}; + +mod about; +mod stats; pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { - TopBottomPanel::top("help_menu").show(ctx, |ui| { - ui.horizontal(|ui| { - ui.selectable_value(&mut app.page, Page::HelpHelp, "Help"); - ui.separator(); - ui.selectable_value(&mut app.page, Page::HelpAbout, "About"); - ui.separator(); - }); + ui.horizontal(|ui| { + ui.selectable_value(&mut app.page, Page::HelpHelp, "Help"); + ui.separator(); + ui.selectable_value(&mut app.page, Page::HelpStats, "Stats"); + ui.separator(); + ui.selectable_value(&mut app.page, Page::HelpAbout, "About"); + ui.separator(); }); + ui.separator(); if app.page == Page::HelpHelp { ui.add_space(24.0); @@ -127,71 +131,9 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.add_space(10.0); }); + } else if app.page == Page::HelpStats { + stats::update(app, ctx, _frame, ui); } else if app.page == Page::HelpAbout { - ui.with_layout(Layout::top_down(Align::Center), |ui| { - - ui.add_space(30.0); - - ui.image(&app.icon, app.icon.size_vec2()); - - ui.add_space(15.0); - - ui.label( - RichText::new(&app.about.name).strong() - ); - - ui.add_space(15.0); - - ui.label( - RichText::new(&app.about.version) - .text_style(TextStyle::Body) - ); - - ui.add_space(15.0); - - ui.label( - RichText::new(&app.about.description) - .text_style(TextStyle::Body) - ); - - ui.add_space(35.0); - - ui.label( - RichText::new(format!("nostr is a protocol and specification for storing and retrieving social media events onto servers called relays. Many users store their events onto multiple relays for reliability, censorship resistance, and to spread their reach. If you didn't store an event on a particular relay, don't expect anyone to find it there because relays normally don't share events with each other. - -Users are defined by their keypair, and are known by the public key of that pair. All events they generate are signed by their private key, and verifiable by their public key. - -We are storing data on your system in this file: {}. This data is only used locally by this client - the nostr protocol does not use clients as a store of data for other people. We are storing your settings, your private and public key, information about relays, and a cache of events. We cache events in your feed so that we don't have to ask relays for them again, which means less network traffic and faster startup times. -", app.about.storage_path)) - .text_style(TextStyle::Body) - ); - - ui.add_space(22.0); - - ui.hyperlink_to("Learn More about Nostr", "https://github.com/nostr-protocol/nostr"); - - ui.add_space(30.0); - - ui.hyperlink_to("Source Code", &app.about.homepage); - ui.label( - RichText::new("by") - .text_style(TextStyle::Small) - ); - ui.label( - RichText::new(&app.about.authors) - .text_style(TextStyle::Small) - ); - - ui.add_space(15.0); - - ui.label( - RichText::new("This program comes with absolutely no warranty.") - .text_style(TextStyle::Small) - ); - ui.label( - RichText::new("See the MIT License for details.") - .text_style(TextStyle::Small) - ); - }); + about::update(app, ctx, _frame, ui); } } diff --git a/src/ui/stats.rs b/src/ui/help/stats.rs similarity index 100% rename from src/ui/stats.rs rename to src/ui/help/stats.rs diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9ae597f2..6c5c88ce 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -3,7 +3,6 @@ mod help; mod people; mod relays; mod settings; -mod stats; mod style; mod widgets; mod you; @@ -16,8 +15,8 @@ use crate::settings::Settings; use crate::ui::widgets::CopyButton; use eframe::{egui, IconData, Theme}; use egui::{ - ColorImage, Context, ImageData, Label, RichText, Sense, TextStyle, TextureHandle, - TextureOptions, Ui, + ColorImage, Context, ImageData, Label, RichText, SelectableLabel, Sense, TextStyle, + TextureHandle, TextureOptions, Ui, }; use nostr_types::{Id, PublicKey, PublicKeyHex}; use std::collections::HashMap; @@ -55,15 +54,17 @@ pub fn run() -> Result<(), Error> { #[derive(PartialEq)] enum Page { - Feed, - PeopleFollow, + FeedGeneral, + FeedThread, + FeedPerson, PeopleList, + PeopleFollow, Person, You, Relays, Settings, - Stats, HelpHelp, + HelpStats, HelpAbout, } @@ -88,6 +89,8 @@ struct GossipUi { person_view_pubkey: Option, avatars: HashMap, new_relay_url: String, + feed_thread_id: Option, + feed_person_pubkey: Option, } impl Drop for GossipUi { @@ -140,7 +143,7 @@ impl GossipUi { GossipUi { next_frame: Instant::now(), - page: Page::Feed, + page: Page::FeedGeneral, status: "Welcome to Gossip. Status messages will appear here. Click them to dismiss them." .to_owned(), @@ -161,6 +164,8 @@ impl GossipUi { person_view_pubkey: None, avatars: HashMap::new(), new_relay_url: "".to_owned(), + feed_thread_id: None, + feed_person_pubkey: None, } } } @@ -187,19 +192,65 @@ impl eframe::App for GossipUi { egui::TopBottomPanel::top("menu").show(ctx, |ui| { ui.horizontal(|ui| { - ui.selectable_value(&mut self.page, Page::Feed, "Feed"); + if ui + .add(SelectableLabel::new( + self.page == Page::FeedGeneral + || self.page == Page::FeedThread + || self.page == Page::FeedPerson, + "Feed", + )) + .clicked() + { + self.page = Page::FeedGeneral; + } ui.separator(); - ui.selectable_value(&mut self.page, Page::PeopleList, "People"); + if ui + .add(SelectableLabel::new( + self.page == Page::PeopleList + || self.page == Page::PeopleFollow + || self.page == Page::Person, + "People", + )) + .clicked() + { + self.page = Page::PeopleList; + } ui.separator(); - ui.selectable_value(&mut self.page, Page::You, "You"); + if ui + .add(SelectableLabel::new(self.page == Page::You, "You")) + .clicked() + { + self.page = Page::You; + } ui.separator(); - ui.selectable_value(&mut self.page, Page::Relays, "Relays"); + if ui + .add(SelectableLabel::new(self.page == Page::Relays, "Relays")) + .clicked() + { + self.page = Page::Relays; + } ui.separator(); - ui.selectable_value(&mut self.page, Page::Settings, "Settings"); + if ui + .add(SelectableLabel::new( + self.page == Page::Settings, + "Settings", + )) + .clicked() + { + self.page = Page::Settings; + } ui.separator(); - ui.selectable_value(&mut self.page, Page::Stats, "Stats"); - ui.separator(); - ui.selectable_value(&mut self.page, Page::HelpHelp, "Help"); + if ui + .add(SelectableLabel::new( + self.page == Page::HelpHelp + || self.page == Page::HelpStats + || self.page == Page::HelpAbout, + "Help", + )) + .clicked() + { + self.page = Page::HelpHelp; + } ui.separator(); }); }); @@ -216,16 +267,18 @@ impl eframe::App for GossipUi { }); egui::CentralPanel::default().show(ctx, |ui| match self.page { - Page::Feed => feed::update(self, ctx, frame, ui), - Page::PeopleList => people::update(self, ctx, frame, ui), - Page::PeopleFollow => people::update(self, ctx, frame, ui), - Page::Person => people::update(self, ctx, frame, ui), + Page::FeedGeneral | Page::FeedThread | Page::FeedPerson => { + feed::update(self, ctx, frame, ui) + } + Page::PeopleList | Page::PeopleFollow | Page::Person => { + people::update(self, ctx, frame, ui) + } Page::You => you::update(self, ctx, frame, ui), Page::Relays => relays::update(self, ctx, frame, ui), Page::Settings => settings::update(self, ctx, frame, ui, darkmode), - Page::Stats => stats::update(self, ctx, frame, ui), - Page::HelpHelp => help::update(self, ctx, frame, ui), - Page::HelpAbout => help::update(self, ctx, frame, ui), + Page::HelpHelp | Page::HelpStats | Page::HelpAbout => { + help::update(self, ctx, frame, ui) + } }); } } diff --git a/src/ui/people.rs b/src/ui/people.rs deleted file mode 100644 index a390c3da..00000000 --- a/src/ui/people.rs +++ /dev/null @@ -1,237 +0,0 @@ -use super::{GossipUi, Page}; -use crate::comms::BusMessage; -use crate::db::DbPerson; -use crate::globals::GLOBALS; -use eframe::egui; -use egui::{Context, Image, RichText, ScrollArea, Sense, TextEdit, TopBottomPanel, Ui, Vec2}; - -pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { - let maybe_person = if let Some(pubkeyhex) = &app.person_view_pubkey { - GLOBALS.people.blocking_write().get(pubkeyhex) - } else { - None - }; - - TopBottomPanel::top("people_menu").show(ctx, |ui| { - ui.horizontal(|ui| { - ui.selectable_value(&mut app.page, Page::PeopleList, "Followed"); - ui.separator(); - ui.selectable_value(&mut app.page, Page::PeopleFollow, "Follow Someone New"); - ui.separator(); - if let Some(person) = &maybe_person { - ui.selectable_value(&mut app.page, Page::Person, get_name(person)); - ui.separator(); - } - }); - }); - - if app.page == Page::PeopleFollow { - ui.add_space(30.0); - - ui.heading("NOTICE: Gossip doesn't update the filters when you follow someone yet, so you have to restart the client to fetch their events. Will fix soon."); - - ui.heading("NOTICE: Gossip is not synchronizing with data on the nostr relays. This is a separate list and it won't overwrite anything."); - - ui.label("NOTICE: use CTRL-V to paste (middle/right click wont work)"); - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - ui.heading("NIP-35: Follow a DNS ID"); - - ui.horizontal(|ui| { - ui.label("Enter user@domain"); - ui.add(TextEdit::singleline(&mut app.nip35follow).hint_text("user@domain")); - }); - if ui.button("follow").clicked() { - let tx = GLOBALS.to_overlord.clone(); - let _ = tx.send(BusMessage { - target: "overlord".to_string(), - kind: "follow_nip35".to_string(), - json_payload: serde_json::to_string(&app.nip35follow).unwrap(), - }); - app.nip35follow = "".to_owned(); - } - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - ui.heading("Follow a bech32 public key"); - - ui.horizontal(|ui| { - ui.label("Enter bech32 public key"); - ui.add(TextEdit::singleline(&mut app.follow_bech32_pubkey).hint_text("npub1...")); - }); - ui.horizontal(|ui| { - ui.label("Enter a relay URL where we can find them"); - ui.add(TextEdit::singleline(&mut app.follow_pubkey_at_relay).hint_text("wss://...")); - }); - if ui.button("follow").clicked() { - let tx = GLOBALS.to_overlord.clone(); - let _ = tx.send(BusMessage { - target: "overlord".to_string(), - kind: "follow_bech32".to_string(), - json_payload: serde_json::to_string(&( - &app.follow_bech32_pubkey, - &app.follow_pubkey_at_relay, - )) - .unwrap(), - }); - app.follow_bech32_pubkey = "".to_owned(); - app.follow_pubkey_at_relay = "".to_owned(); - } - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - ui.heading("Follow a hex public key"); - - ui.horizontal(|ui| { - ui.label("Enter hex-encoded public key"); - ui.add( - TextEdit::singleline(&mut app.follow_hex_pubkey).hint_text("0123456789abcdef..."), - ); - }); - ui.horizontal(|ui| { - ui.label("Enter a relay URL where we can find them"); - ui.add(TextEdit::singleline(&mut app.follow_pubkey_at_relay).hint_text("wss://...")); - }); - if ui.button("follow").clicked() { - let tx = GLOBALS.to_overlord.clone(); - let _ = tx.send(BusMessage { - target: "overlord".to_string(), - kind: "follow_hexkey".to_string(), - json_payload: serde_json::to_string(&( - &app.follow_hex_pubkey, - &app.follow_pubkey_at_relay, - )) - .unwrap(), - }); - app.follow_hex_pubkey = "".to_owned(); - app.follow_pubkey_at_relay = "".to_owned(); - } - } else if app.page == Page::PeopleList { - ui.add_space(24.0); - - ui.heading("NOTICE: Gossip is not synchronizing with data on the nostr relays. This is a separate list and it won't overwrite anything."); - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - ui.heading("People Followed"); - ui.add_space(18.0); - - let people = GLOBALS.people.blocking_write().get_all(); - - ScrollArea::vertical().show(ui, |ui| { - for person in people.iter() { - if person.followed != 1 { - continue; - } - - ui.horizontal(|ui| { - // Avatar first - let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) { - avatar - } else { - app.placeholder_avatar.clone() - }; - if ui - .add( - Image::new( - &avatar, - Vec2 { - x: crate::AVATAR_SIZE_F32, - y: crate::AVATAR_SIZE_F32, - }, - ) - .sense(Sense::click()), - ) - .clicked() - { - set_person_view(app, person); - }; - - ui.vertical(|ui| { - ui.label(RichText::new(GossipUi::hex_pubkey_short(&person.pubkey)).weak()); - GossipUi::render_person_name_line(ui, Some(person)); - }); - }); - - ui.add_space(4.0); - - ui.separator(); - } - }); - } else if app.page == Page::Person { - if maybe_person.is_none() || app.person_view_pubkey.is_none() { - ui.label("ERROR"); - } else { - let person = maybe_person.as_ref().unwrap(); - let pubkeyhex = app.person_view_pubkey.as_ref().unwrap().clone(); - - ui.add_space(24.0); - - ui.heading(get_name(person)); - - ui.horizontal(|ui| { - // Avatar first - let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &pubkeyhex) { - avatar - } else { - app.placeholder_avatar.clone() - }; - ui.image(&avatar, Vec2 { x: 36.0, y: 36.0 }); - - ui.vertical(|ui| { - ui.label(RichText::new(GossipUi::hex_pubkey_short(&pubkeyhex)).weak()); - GossipUi::render_person_name_line(ui, Some(person)); - }); - }); - - ui.add_space(12.0); - - if let Some(about) = person.about.as_deref() { - ui.label(about); - } - - ui.add_space(12.0); - - #[allow(clippy::collapsible_else_if)] - if person.followed == 0 { - if ui.button("FOLLOW").clicked() { - GLOBALS.people.blocking_write().follow(&pubkeyhex, true); - } - } else { - if ui.button("UNFOLLOW").clicked() { - GLOBALS.people.blocking_write().follow(&pubkeyhex, false); - } - } - - if ui.button("UPDATE METADATA").clicked() { - let _ = GLOBALS.to_overlord.send(BusMessage { - target: "overlord".to_string(), - kind: "update_metadata".to_string(), - json_payload: serde_json::to_string(&pubkeyhex).unwrap(), - }); - } - } - } -} - -fn get_name(person: &DbPerson) -> String { - if let Some(name) = &person.name { - name.to_owned() - } else { - GossipUi::hex_pubkey_short(&person.pubkey) - } -} - -fn set_person_view(app: &mut GossipUi, person: &DbPerson) { - app.person_view_pubkey = Some(person.pubkey.clone()); - app.page = Page::Person; -} diff --git a/src/ui/people/follow.rs b/src/ui/people/follow.rs new file mode 100644 index 00000000..00a554d3 --- /dev/null +++ b/src/ui/people/follow.rs @@ -0,0 +1,93 @@ +use super::GossipUi; +use crate::comms::BusMessage; +use crate::globals::GLOBALS; +use eframe::egui; +use egui::{Context, TextEdit, Ui}; + +pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { + ui.add_space(30.0); + + ui.heading("NOTICE: Gossip doesn't update the filters when you follow someone yet, so you have to restart the client to fetch their events. Will fix soon."); + + ui.heading("NOTICE: Gossip is not synchronizing with data on the nostr relays. This is a separate list and it won't overwrite anything."); + + ui.label("NOTICE: use CTRL-V to paste (middle/right click wont work)"); + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + ui.heading("NIP-35: Follow a DNS ID"); + + ui.horizontal(|ui| { + ui.label("Enter user@domain"); + ui.add(TextEdit::singleline(&mut app.nip35follow).hint_text("user@domain")); + }); + if ui.button("follow").clicked() { + let tx = GLOBALS.to_overlord.clone(); + let _ = tx.send(BusMessage { + target: "overlord".to_string(), + kind: "follow_nip35".to_string(), + json_payload: serde_json::to_string(&app.nip35follow).unwrap(), + }); + app.nip35follow = "".to_owned(); + } + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + ui.heading("Follow a bech32 public key"); + + ui.horizontal(|ui| { + ui.label("Enter bech32 public key"); + ui.add(TextEdit::singleline(&mut app.follow_bech32_pubkey).hint_text("npub1...")); + }); + ui.horizontal(|ui| { + ui.label("Enter a relay URL where we can find them"); + ui.add(TextEdit::singleline(&mut app.follow_pubkey_at_relay).hint_text("wss://...")); + }); + if ui.button("follow").clicked() { + let tx = GLOBALS.to_overlord.clone(); + let _ = tx.send(BusMessage { + target: "overlord".to_string(), + kind: "follow_bech32".to_string(), + json_payload: serde_json::to_string(&( + &app.follow_bech32_pubkey, + &app.follow_pubkey_at_relay, + )) + .unwrap(), + }); + app.follow_bech32_pubkey = "".to_owned(); + app.follow_pubkey_at_relay = "".to_owned(); + } + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + ui.heading("Follow a hex public key"); + + ui.horizontal(|ui| { + ui.label("Enter hex-encoded public key"); + ui.add(TextEdit::singleline(&mut app.follow_hex_pubkey).hint_text("0123456789abcdef...")); + }); + ui.horizontal(|ui| { + ui.label("Enter a relay URL where we can find them"); + ui.add(TextEdit::singleline(&mut app.follow_pubkey_at_relay).hint_text("wss://...")); + }); + if ui.button("follow").clicked() { + let tx = GLOBALS.to_overlord.clone(); + let _ = tx.send(BusMessage { + target: "overlord".to_string(), + kind: "follow_hexkey".to_string(), + json_payload: serde_json::to_string(&( + &app.follow_hex_pubkey, + &app.follow_pubkey_at_relay, + )) + .unwrap(), + }); + app.follow_hex_pubkey = "".to_owned(); + app.follow_pubkey_at_relay = "".to_owned(); + } +} diff --git a/src/ui/people/mod.rs b/src/ui/people/mod.rs new file mode 100644 index 00000000..861412a2 --- /dev/null +++ b/src/ui/people/mod.rs @@ -0,0 +1,101 @@ +use super::{GossipUi, Page}; +use crate::db::DbPerson; +use crate::globals::GLOBALS; +use eframe::egui; +use egui::{Context, Image, RichText, ScrollArea, Sense, Ui, Vec2}; + +mod follow; +mod person; + +pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { + let maybe_person = if let Some(pubkeyhex) = &app.person_view_pubkey { + GLOBALS.people.blocking_write().get(pubkeyhex) + } else { + None + }; + + ui.horizontal(|ui| { + ui.selectable_value(&mut app.page, Page::PeopleList, "Followed"); + ui.separator(); + ui.selectable_value(&mut app.page, Page::PeopleFollow, "Follow Someone New"); + ui.separator(); + if let Some(person) = &maybe_person { + ui.selectable_value(&mut app.page, Page::Person, get_name(person)); + ui.separator(); + } + }); + ui.separator(); + + if app.page == Page::PeopleList { + ui.add_space(24.0); + + ui.heading("NOTICE: Gossip is not synchronizing with data on the nostr relays. This is a separate list and it won't overwrite anything."); + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + ui.heading("People Followed"); + ui.add_space(18.0); + + let people = GLOBALS.people.blocking_write().get_all(); + + ScrollArea::vertical().show(ui, |ui| { + for person in people.iter() { + if person.followed != 1 { + continue; + } + + ui.horizontal(|ui| { + // Avatar first + let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) { + avatar + } else { + app.placeholder_avatar.clone() + }; + if ui + .add( + Image::new( + &avatar, + Vec2 { + x: crate::AVATAR_SIZE_F32, + y: crate::AVATAR_SIZE_F32, + }, + ) + .sense(Sense::click()), + ) + .clicked() + { + set_person_view(app, person); + }; + + ui.vertical(|ui| { + ui.label(RichText::new(GossipUi::hex_pubkey_short(&person.pubkey)).weak()); + GossipUi::render_person_name_line(ui, Some(person)); + }); + }); + + ui.add_space(4.0); + + ui.separator(); + } + }); + } else if app.page == Page::PeopleFollow { + follow::update(app, ctx, _frame, ui); + } else if app.page == Page::Person { + person::update(app, ctx, _frame, ui); + } +} + +fn get_name(person: &DbPerson) -> String { + if let Some(name) = &person.name { + name.to_owned() + } else { + GossipUi::hex_pubkey_short(&person.pubkey) + } +} + +fn set_person_view(app: &mut GossipUi, person: &DbPerson) { + app.person_view_pubkey = Some(person.pubkey.clone()); + app.page = Page::Person; +} diff --git a/src/ui/people/person.rs b/src/ui/people/person.rs new file mode 100644 index 00000000..119ff17f --- /dev/null +++ b/src/ui/people/person.rs @@ -0,0 +1,75 @@ +use super::GossipUi; +use crate::comms::BusMessage; +use crate::db::DbPerson; +use crate::globals::GLOBALS; +use eframe::egui; +use egui::{Context, RichText, Ui, Vec2}; + +pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { + let maybe_person = if let Some(pubkeyhex) = &app.person_view_pubkey { + GLOBALS.people.blocking_write().get(pubkeyhex) + } else { + None + }; + + if maybe_person.is_none() || app.person_view_pubkey.is_none() { + ui.label("ERROR"); + } else { + let person = maybe_person.as_ref().unwrap(); + let pubkeyhex = app.person_view_pubkey.as_ref().unwrap().clone(); + + ui.add_space(24.0); + + ui.heading(get_name(person)); + + ui.horizontal(|ui| { + // Avatar first + let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &pubkeyhex) { + avatar + } else { + app.placeholder_avatar.clone() + }; + ui.image(&avatar, Vec2 { x: 36.0, y: 36.0 }); + + ui.vertical(|ui| { + ui.label(RichText::new(GossipUi::hex_pubkey_short(&pubkeyhex)).weak()); + GossipUi::render_person_name_line(ui, Some(person)); + }); + }); + + ui.add_space(12.0); + + if let Some(about) = person.about.as_deref() { + ui.label(about); + } + + ui.add_space(12.0); + + #[allow(clippy::collapsible_else_if)] + if person.followed == 0 { + if ui.button("FOLLOW").clicked() { + GLOBALS.people.blocking_write().follow(&pubkeyhex, true); + } + } else { + if ui.button("UNFOLLOW").clicked() { + GLOBALS.people.blocking_write().follow(&pubkeyhex, false); + } + } + + if ui.button("UPDATE METADATA").clicked() { + let _ = GLOBALS.to_overlord.send(BusMessage { + target: "overlord".to_string(), + kind: "update_metadata".to_string(), + json_payload: serde_json::to_string(&pubkeyhex).unwrap(), + }); + } + } +} + +fn get_name(person: &DbPerson) -> String { + if let Some(name) = &person.name { + name.to_owned() + } else { + GossipUi::hex_pubkey_short(&person.pubkey) + } +} From 0174bd2751c9186b8624834aa6bb82196bb9d17e Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 3 Jan 2023 10:59:06 +1300 Subject: [PATCH 2/8] Remove settings.view_threaded, force non-threaded feed for the moment --- src/feed.rs | 16 +++++++--------- src/settings.rs | 6 ------ src/ui/feed/mod.rs | 2 +- src/ui/settings.rs | 4 ---- 4 files changed, 8 insertions(+), 20 deletions(-) diff --git a/src/feed.rs b/src/feed.rs index 436cb24e..4ca5d693 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -87,16 +87,13 @@ impl Feed { let mut events: Vec = events .iter() .filter(|e| !GLOBALS.dismissed.blocking_read().contains(&e.id)) - .filter(|e| { - if settings.view_threaded { - e.replies_to().is_none() - } else { - true - } - }) + //.filter(|e| { for Threaded + //e.replies_to().is_none() + //}) .cloned() .collect(); + /* for threaded if settings.view_threaded { events.sort_unstable_by(|a, b| { let a_last = GLOBALS.last_reply.blocking_read().get(&a.id).cloned(); @@ -105,9 +102,10 @@ impl Feed { let b_time = b_last.unwrap_or(b.created_at); b_time.cmp(&a_time) }); - } else { - events.sort_unstable_by(|a, b| b.created_at.cmp(&a.created_at)); } + */ + + events.sort_unstable_by(|a, b| b.created_at.cmp(&a.created_at)); self.feed = events.iter().map(|e| e.id).collect(); } diff --git a/src/settings.rs b/src/settings.rs index 87bff0ec..89e9490b 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -7,7 +7,6 @@ pub const DEFAULT_FEED_CHUNK: u64 = 43200; // 12 hours pub const DEFAULT_OVERLAP: u64 = 600; // 10 minutes pub const DEFAULT_VIEW_POSTS_REFERRED_TO: bool = true; pub const DEFAULT_VIEW_POSTS_REFERRING_TO: bool = false; -pub const DEFAULT_VIEW_THREADED: bool = true; pub const DEFAULT_NUM_RELAYS_PER_PERSON: u8 = 4; pub const DEFAULT_MAX_RELAYS: u8 = 15; pub const DEFAULT_MAX_FPS: u32 = 30; @@ -21,7 +20,6 @@ pub struct Settings { pub overlap: u64, pub view_posts_referred_to: bool, pub view_posts_referring_to: bool, - pub view_threaded: bool, pub num_relays_per_person: u8, pub max_relays: u8, pub public_key: Option, @@ -39,7 +37,6 @@ impl Default for Settings { overlap: DEFAULT_OVERLAP, view_posts_referred_to: DEFAULT_VIEW_POSTS_REFERRED_TO, view_posts_referring_to: DEFAULT_VIEW_POSTS_REFERRING_TO, - view_threaded: DEFAULT_VIEW_THREADED, num_relays_per_person: DEFAULT_NUM_RELAYS_PER_PERSON, max_relays: DEFAULT_MAX_RELAYS, public_key: None, @@ -76,7 +73,6 @@ impl Settings { "view_posts_referring_to" => { settings.view_posts_referring_to = numstr_to_bool(row.1) } - "view_threaded" => settings.view_threaded = numstr_to_bool(row.1), "num_relays_per_person" => { settings.num_relays_per_person = row.1.parse::().unwrap_or(DEFAULT_NUM_RELAYS_PER_PERSON) @@ -130,7 +126,6 @@ impl Settings { ('overlap', ?),\ ('view_posts_referred_to', ?),\ ('view_posts_referring_to', ?),\ - ('view_threaded', ?),\ ('num_relays_per_person', ?),\ ('max_relays', ?),\ ('max_fps', ?),\ @@ -143,7 +138,6 @@ impl Settings { self.overlap, bool_to_numstr(self.view_posts_referred_to), bool_to_numstr(self.view_posts_referring_to), - bool_to_numstr(self.view_threaded), self.num_relays_per_person, self.max_relays, self.max_fps, diff --git a/src/ui/feed/mod.rs b/src/ui/feed/mod.rs index 6080adf4..e901dac5 100644 --- a/src/ui/feed/mod.rs +++ b/src/ui/feed/mod.rs @@ -166,7 +166,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram ui.separator(); - let threaded = GLOBALS.settings.blocking_read().view_threaded; + let threaded = false; ScrollArea::vertical().show(ui, |ui| { let bgcolor = if ctx.style().visuals.dark_mode { diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 49c506e7..7e712af6 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -57,10 +57,6 @@ pub(super) fn update( ui.heading("Feed"); - ui.checkbox(&mut app.settings.view_threaded, "Threaded feed") - .on_hover_text("If selected, replies are under what they reply to and the newest replied-to thread comes first. Otherwise all posts are independent and in time order."); - - ui.add_space(24.0); ui.horizontal(|ui| { ui.label("Recompute feed every (milliseconds): ") .on_hover_text( From 238c429017ad4ceabe39ae037956e8504453e621 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 3 Jan 2023 11:17:47 +1300 Subject: [PATCH 3/8] Feed: switch to interior mutability --- Cargo.lock | 1 + Cargo.toml | 1 + src/feed.rs | 45 +++++++++++++++++++++++---------------------- src/globals.rs | 4 ++-- src/ui/feed/mod.rs | 2 +- 5 files changed, 28 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f177a47e..eaeba4b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1697,6 +1697,7 @@ dependencies = [ "lazy_static", "linkify", "nostr-types", + "parking_lot", "rand 0.8.5", "reqwest", "rusqlite", diff --git a/Cargo.toml b/Cargo.toml index 7e6ce072..5d17cc77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ image = "0.24" lazy_static = "1.4" linkify = "0.9" nostr-types = { git = "https://github.com/mikedilger/nostr-types" } +parking_lot = "0.12" rand = "0.8" reqwest = { version = "0.11", features = ["json"] } rusqlite = { version = "0.28", features = ["bundled", "chrono", "serde_json"] } diff --git a/src/feed.rs b/src/feed.rs index 4ca5d693..f9fdfefc 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -1,55 +1,56 @@ use crate::globals::GLOBALS; use nostr_types::{Event, EventKind, Id}; use std::time::{Duration, Instant}; +use parking_lot::RwLock; pub struct Feed { - feed: Vec, + feed: RwLock>, // We only recompute the feed at specified intervals - interval_ms: u32, - last_computed: Instant, + interval_ms: RwLock, + last_computed: RwLock, // We track these to update subscriptions on them - my_event_ids: Vec, - followed_event_ids: Vec, + my_event_ids: RwLock>, + followed_event_ids: RwLock>, } impl Feed { pub fn new() -> Feed { Feed { - feed: Vec::new(), - interval_ms: 1000, // Every second, until we load from settings - last_computed: Instant::now(), - my_event_ids: Vec::new(), - followed_event_ids: Vec::new(), + feed: RwLock::new(Vec::new()), + interval_ms: RwLock::new(1000), // Every second, until we load from settings + last_computed: RwLock::new(Instant::now()), + my_event_ids: RwLock::new(Vec::new()), + followed_event_ids: RwLock::new(Vec::new()), } } - pub fn get(&mut self) -> Vec { + pub fn get(&self) -> Vec { let now = Instant::now(); - if self.last_computed + Duration::from_millis(self.interval_ms as u64) < now { + if *self.last_computed.read() + Duration::from_millis(*self.interval_ms.read() as u64) < now { self.recompute(); - self.last_computed = now; + *self.last_computed.write() = now; } - self.feed.clone() + self.feed.read().clone() } #[allow(dead_code)] pub fn get_my_event_ids(&self) -> Vec { // we assume the main get() happens fast enough to recompute for us. - self.my_event_ids.clone() + self.my_event_ids.read().clone() } #[allow(dead_code)] pub fn get_followed_event_ids(&self) -> Vec { // we assume the main get() happens fast enough to recompute for us. - self.followed_event_ids.clone() + self.followed_event_ids.read().clone() } - fn recompute(&mut self) { + fn recompute(&self) { let settings = GLOBALS.settings.blocking_read().clone(); - self.interval_ms = settings.feed_recompute_interval_ms; + *self.interval_ms.write() = settings.feed_recompute_interval_ms; let events: Vec = GLOBALS .events @@ -62,17 +63,17 @@ impl Feed { // My event ids if let Some(pubkey) = GLOBALS.signer.blocking_read().public_key() { - self.my_event_ids = events + *self.my_event_ids.write() = events .iter() .filter_map(|e| if e.pubkey == pubkey { Some(e.id) } else { None }) .collect(); } else { - self.my_event_ids = vec![]; + *self.my_event_ids.write() = vec![]; } // Followed event ids let followed_pubkeys = GLOBALS.people.blocking_read().get_followed_pubkeys(); - self.followed_event_ids = events + *self.followed_event_ids.write() = events .iter() .filter_map(|e| { if followed_pubkeys.contains(&e.pubkey.into()) { @@ -107,6 +108,6 @@ impl Feed { events.sort_unstable_by(|a, b| b.created_at.cmp(&a.created_at)); - self.feed = events.iter().map(|e| e.id).collect(); + *self.feed.write() = events.iter().map(|e| e.id).collect(); } } diff --git a/src/globals.rs b/src/globals.rs index c071ceb1..b3f6ab10 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -71,7 +71,7 @@ pub struct Globals { pub event_is_new: RwLock>, /// Feed - pub feed: Mutex, + pub feed: Feed, /// Fetcher pub fetcher: Fetcher, @@ -106,7 +106,7 @@ lazy_static! { signer: RwLock::new(Signer::default()), dismissed: RwLock::new(Vec::new()), event_is_new: RwLock::new(Vec::new()), - feed: Mutex::new(Feed::new()), + feed: Feed::new(), fetcher: Fetcher::new(), failed_avatars: RwLock::new(HashSet::new()), } diff --git a/src/ui/feed/mod.rs b/src/ui/feed/mod.rs index e901dac5..ba533049 100644 --- a/src/ui/feed/mod.rs +++ b/src/ui/feed/mod.rs @@ -36,7 +36,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram ui.separator(); if app.page == Page::FeedGeneral { - let feed = GLOBALS.feed.blocking_lock().get(); + let feed = GLOBALS.feed.get(); Globals::trim_desired_events_sync(); let desired_count: isize = match GLOBALS.desired_events.try_read() { From 611b5ab12c7ce189e63852f4d4f112a59fab4591 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 3 Jan 2023 13:46:57 +1300 Subject: [PATCH 4/8] Multiple feeds working --- src/feed.rs | 103 +++++++-- src/ui/{feed/mod.rs => feed.rs} | 379 +++++++++++++++++--------------- src/ui/feed/person.rs | 7 - src/ui/feed/thread.rs | 7 - src/ui/mod.rs | 6 - src/ui/people/person.rs | 7 +- 6 files changed, 287 insertions(+), 222 deletions(-) rename src/ui/{feed/mod.rs => feed.rs} (59%) delete mode 100644 src/ui/feed/person.rs delete mode 100644 src/ui/feed/thread.rs diff --git a/src/feed.rs b/src/feed.rs index f9fdfefc..87e41514 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -1,10 +1,20 @@ use crate::globals::GLOBALS; +use nostr_types::PublicKeyHex; use nostr_types::{Event, EventKind, Id}; -use std::time::{Duration, Instant}; use parking_lot::RwLock; +use std::time::{Duration, Instant}; + +#[derive(Clone, Debug)] +pub enum FeedKind { + General, + Thread(Id), + Person(PublicKeyHex), +} pub struct Feed { - feed: RwLock>, + current_feed_kind: RwLock, + + general_feed: RwLock>, // We only recompute the feed at specified intervals interval_ms: RwLock, @@ -18,7 +28,8 @@ pub struct Feed { impl Feed { pub fn new() -> Feed { Feed { - feed: RwLock::new(Vec::new()), + current_feed_kind: RwLock::new(FeedKind::General), + general_feed: RwLock::new(Vec::new()), interval_ms: RwLock::new(1000), // Every second, until we load from settings last_computed: RwLock::new(Instant::now()), my_event_ids: RwLock::new(Vec::new()), @@ -26,14 +37,77 @@ impl Feed { } } - pub fn get(&self) -> Vec { + pub fn set_feed_to_general(&self) { + *self.current_feed_kind.write() = FeedKind::General; + } + + pub fn set_feed_to_thread(&self, id: Id) { + // get parent? + *self.current_feed_kind.write() = FeedKind::Thread(id); + } + + pub fn set_feed_to_person(&self, pubkey: PublicKeyHex) { + // FIXME - TRIGGER OVERLORD TO FETCH THEIR EVENTS FURTHER BACK + *self.current_feed_kind.write() = FeedKind::Person(pubkey); + } + + pub fn get_feed_kind(&self) -> FeedKind { + self.current_feed_kind.read().to_owned() + } + + pub fn get_general(&self) -> Vec { let now = Instant::now(); - if *self.last_computed.read() + Duration::from_millis(*self.interval_ms.read() as u64) < now { + if *self.last_computed.read() + Duration::from_millis(*self.interval_ms.read() as u64) < now + { self.recompute(); *self.last_computed.write() = now; } - self.feed.read().clone() + self.general_feed.read().clone() + } + + pub fn get_thread_parent(&self, id: Id) -> Id { + // FIXME - TRIGGER OVERLORD TO FETCH THIS FEED + let mut event = match GLOBALS.events.blocking_read().get(&id).cloned() { + None => return id, + Some(e) => e, + }; + + // Try for root + if let Some((root, _)) = event.replies_to_root() { + if GLOBALS.events.blocking_read().contains_key(&root) { + return root; + } + } + + // Climb parents as high as we can + while let Some((parent, _)) = event.replies_to() { + if let Some(e) = GLOBALS.events.blocking_read().get(&parent) { + event = e.to_owned(); + } else { + break; + } + } + + // The highest event id we have + event.id + } + + pub fn get_person_feed(&self, person: PublicKeyHex) -> Vec { + let mut events: Vec = GLOBALS + .events + .blocking_read() + .iter() + .map(|(_, e)| e) + .filter(|e| e.kind == EventKind::TextNote) + .filter(|e| e.pubkey.as_hex_string() == person.0) + .filter(|e| !GLOBALS.dismissed.blocking_read().contains(&e.id)) + .map(|e| e.to_owned()) + .collect(); + + events.sort_unstable_by(|a, b| b.created_at.cmp(&a.created_at)); + + events.iter().map(|e| e.id).collect() } #[allow(dead_code)] @@ -88,26 +162,11 @@ impl Feed { let mut events: Vec = events .iter() .filter(|e| !GLOBALS.dismissed.blocking_read().contains(&e.id)) - //.filter(|e| { for Threaded - //e.replies_to().is_none() - //}) .cloned() .collect(); - /* for threaded - if settings.view_threaded { - events.sort_unstable_by(|a, b| { - let a_last = GLOBALS.last_reply.blocking_read().get(&a.id).cloned(); - let b_last = GLOBALS.last_reply.blocking_read().get(&b.id).cloned(); - let a_time = a_last.unwrap_or(a.created_at); - let b_time = b_last.unwrap_or(b.created_at); - b_time.cmp(&a_time) - }); - } - */ - events.sort_unstable_by(|a, b| b.created_at.cmp(&a.created_at)); - *self.feed.write() = events.iter().map(|e| e.id).collect(); + *self.general_feed.write() = events.iter().map(|e| e.id).collect(); } } diff --git a/src/ui/feed/mod.rs b/src/ui/feed.rs similarity index 59% rename from src/ui/feed/mod.rs rename to src/ui/feed.rs index ba533049..0d3e1510 100644 --- a/src/ui/feed/mod.rs +++ b/src/ui/feed.rs @@ -1,18 +1,16 @@ use super::{GossipUi, Page}; use crate::comms::BusMessage; +use crate::feed::FeedKind; use crate::globals::{Globals, GLOBALS}; use crate::ui::widgets::{CopyButton, ReplyButton}; use eframe::egui; use egui::{ - Align, Color32, Context, Frame, Image, Label, Layout, RichText, ScrollArea, Sense, TextEdit, - Ui, Vec2, + Align, Color32, Context, Frame, Image, Layout, RichText, ScrollArea, SelectableLabel, Sense, + TextEdit, Ui, Vec2, }; use linkify::{LinkFinder, LinkKind}; use nostr_types::{EventKind, Id, PublicKeyHex}; -mod person; -mod thread; - struct FeedPostParams { id: Id, indent: usize, @@ -21,183 +19,208 @@ struct FeedPostParams { } pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Frame, ui: &mut Ui) { + let mut feed_kind = GLOBALS.feed.get_feed_kind(); + app.page = match feed_kind { + FeedKind::General => Page::FeedGeneral, + FeedKind::Thread(_) => Page::FeedThread, + FeedKind::Person(_) => Page::FeedPerson, + }; + ui.horizontal(|ui| { - ui.selectable_value(&mut app.page, Page::FeedGeneral, "Following"); + if ui + .add(SelectableLabel::new( + app.page == Page::FeedGeneral, + "Following", + )) + .clicked() + { + app.page = Page::FeedGeneral; + GLOBALS.feed.set_feed_to_general(); + feed_kind = FeedKind::General; + } ui.separator(); - if app.feed_thread_id.is_some() { + if matches!(feed_kind, FeedKind::Thread(..)) { ui.selectable_value(&mut app.page, Page::FeedThread, "Thread"); ui.separator(); } - if app.feed_person_pubkey.is_some() { + if matches!(feed_kind, FeedKind::Person(..)) { ui.selectable_value(&mut app.page, Page::FeedPerson, "Person"); ui.separator(); } }); ui.separator(); - if app.page == Page::FeedGeneral { - let feed = GLOBALS.feed.get(); + Globals::trim_desired_events_sync(); + let desired_count: isize = match GLOBALS.desired_events.try_read() { + Ok(v) => v.len() as isize, + Err(_) => -1, + }; + let incoming_count: isize = match GLOBALS.incoming_events.try_read() { + Ok(v) => v.len() as isize, + Err(_) => -1, + }; - Globals::trim_desired_events_sync(); - let desired_count: isize = match GLOBALS.desired_events.try_read() { - Ok(v) => v.len() as isize, - Err(_) => -1, - }; - let incoming_count: isize = match GLOBALS.incoming_events.try_read() { - Ok(v) => v.len() as isize, - Err(_) => -1, - }; - - ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { - if ui - .button(&format!("QM {}", desired_count)) - .on_hover_text("Query Relays for Missing Events") - .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(), - }); - } - - if ui - .button(&format!("PQ {}", incoming_count)) - .on_hover_text("Process Queue of Incoming Events") - .clicked() - { - let tx = GLOBALS.to_overlord.clone(); - let _ = tx.send(BusMessage { - target: "overlord".to_string(), - kind: "process_incoming_events".to_string(), - json_payload: serde_json::to_string("").unwrap(), - }); - } - - if ui.button("close all").clicked() { - app.hides = feed.clone(); - } - if ui.button("open all").clicked() { - app.hides.clear(); - } - ui.label(&format!( - "RIF={}", - GLOBALS - .fetcher - .requests_in_flight - .load(std::sync::atomic::Ordering::Relaxed) - )); - }); - - 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").clicked() { - app.page = Page::Relays; - } - ui.label(" to post."); - }); - } else { - if let Some(id) = app.replying_to { - render_post_actual( - app, - ctx, - frame, - ui, - FeedPostParams { - id, - indent: 0, - as_reply_to: true, - threaded: false, - }, - ); - } - - 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(); - 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(); - - let threaded = false; - - ScrollArea::vertical().show(ui, |ui| { - let bgcolor = if ctx.style().visuals.dark_mode { - Color32::BLACK - } else { - Color32::WHITE - }; - Frame::none().fill(bgcolor).show(ui, |ui| { - for id in feed.iter() { - render_post_maybe_fake( - app, - ctx, - frame, - ui, - FeedPostParams { - id: *id, - indent: 0, - as_reply_to: false, - threaded, - }, - ); - } + ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { + if ui + .button(&format!("QM {}", desired_count)) + .on_hover_text("Query Relays for Missing Events") + .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(), }); - }); - } else if app.page == Page::FeedThread { - thread::update(app, ctx, frame, ui); - } else if app.page == Page::FeedPerson { - person::update(app, ctx, frame, ui); + } + + if ui + .button(&format!("PQ {}", incoming_count)) + .on_hover_text("Process Queue of Incoming Events") + .clicked() + { + let tx = GLOBALS.to_overlord.clone(); + let _ = tx.send(BusMessage { + target: "overlord".to_string(), + kind: "process_incoming_events".to_string(), + json_payload: serde_json::to_string("").unwrap(), + }); + } + + ui.label(&format!( + "RIF={}", + GLOBALS + .fetcher + .requests_in_flight + .load(std::sync::atomic::Ordering::Relaxed) + )); + }); + + 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").clicked() { + app.page = Page::Relays; + } + ui.label(" to post."); + }); + } else { + if let Some(id) = app.replying_to { + render_post_actual( + app, + ctx, + frame, + ui, + FeedPostParams { + id, + indent: 0, + as_reply_to: true, + threaded: false, + }, + ); + } + + 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(); + 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(); + + match feed_kind { + FeedKind::General => { + let feed = GLOBALS.feed.get_general(); + render_a_feed(app, ctx, frame, ui, feed, false); + } + FeedKind::Thread(id) => { + let parent = GLOBALS.feed.get_thread_parent(id); + render_a_feed(app, ctx, frame, ui, vec![parent], true); + } + FeedKind::Person(pubkeyhex) => { + let feed = GLOBALS.feed.get_person_feed(pubkeyhex); + render_a_feed(app, ctx, frame, ui, feed, false); + } } } +fn render_a_feed( + app: &mut GossipUi, + ctx: &Context, + frame: &mut eframe::Frame, + ui: &mut Ui, + feed: Vec, + threaded: bool, +) { + ScrollArea::vertical().show(ui, |ui| { + let bgcolor = if ctx.style().visuals.dark_mode { + Color32::BLACK + } else { + Color32::WHITE + }; + Frame::none().fill(bgcolor).show(ui, |ui| { + for id in feed.iter() { + render_post_maybe_fake( + app, + ctx, + frame, + ui, + FeedPostParams { + id: *id, + indent: 0, + as_reply_to: false, + threaded, + }, + ); + } + }); + }); +} + fn render_post_maybe_fake( app: &mut GossipUi, ctx: &Context, @@ -238,7 +261,7 @@ fn render_post_maybe_fake( ui.add_space(height); // Yes, and we need to fake render threads to get their approx height too. - if threaded && !as_reply_to && !app.hides.contains(&id) { + if threaded && !as_reply_to { let replies = Globals::get_replies_sync(event.id); for reply_id in replies { render_post_maybe_fake( @@ -352,17 +375,6 @@ fn render_post_actual( ui.horizontal(|ui| { // Indents first (if threaded) if threaded { - #[allow(clippy::collapsible_else_if)] - if app.hides.contains(&id) { - if ui.add(Label::new("▶").sense(Sense::click())).clicked() { - app.hides.retain(|e| *e != id) - } - } else { - if ui.add(Label::new("▼").sense(Sense::click())).clicked() { - app.hides.push(id); - } - } - let space = 16.0 * (10.0 - (100.0 / (indent as f32 + 10.0))); ui.add_space(space); if indent > 0 { @@ -406,6 +418,10 @@ fn render_post_actual( ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { ui.menu_button(RichText::new("≡").size(28.0), |ui| { + if ui.button("View Thread").clicked() { + GLOBALS.feed.set_feed_to_thread(event.id); + app.page = Page::FeedThread; + } if ui.button("Copy ID").clicked() { ui.output().copied_text = event.id.as_hex_string(); } @@ -424,6 +440,11 @@ fn render_post_actual( } }); + if ui.button("➤").clicked() { + GLOBALS.feed.set_feed_to_thread(event.id); + app.page = Page::FeedThread; + } + ui.label( RichText::new(crate::date_ago::date_ago(event.created_at)) .italics() @@ -475,7 +496,7 @@ fn render_post_actual( ui.separator(); - if threaded && !as_reply_to && !app.hides.contains(&id) { + if threaded && !as_reply_to { let replies = Globals::get_replies_sync(event.id); for reply_id in replies { render_post_maybe_fake( diff --git a/src/ui/feed/person.rs b/src/ui/feed/person.rs deleted file mode 100644 index f5c32dff..00000000 --- a/src/ui/feed/person.rs +++ /dev/null @@ -1,7 +0,0 @@ -use super::GossipUi; -use eframe::egui; -use egui::{Context, Ui}; - -pub(super) fn update(_app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { - ui.label("Person TBD"); -} diff --git a/src/ui/feed/thread.rs b/src/ui/feed/thread.rs deleted file mode 100644 index efc07ce2..00000000 --- a/src/ui/feed/thread.rs +++ /dev/null @@ -1,7 +0,0 @@ -use super::GossipUi; -use eframe::egui; -use egui::{Context, Ui}; - -pub(super) fn update(_app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { - ui.label("Thread TBD"); -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 6c5c88ce..922031db 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -85,12 +85,9 @@ struct GossipUi { import_bech32: String, import_hex: String, replying_to: Option, - hides: Vec, person_view_pubkey: Option, avatars: HashMap, new_relay_url: String, - feed_thread_id: Option, - feed_person_pubkey: Option, } impl Drop for GossipUi { @@ -160,12 +157,9 @@ impl GossipUi { import_bech32: "".to_owned(), import_hex: "".to_owned(), replying_to: None, - hides: Vec::new(), person_view_pubkey: None, avatars: HashMap::new(), new_relay_url: "".to_owned(), - feed_thread_id: None, - feed_person_pubkey: None, } } } diff --git a/src/ui/people/person.rs b/src/ui/people/person.rs index 119ff17f..6ef4f720 100644 --- a/src/ui/people/person.rs +++ b/src/ui/people/person.rs @@ -1,4 +1,4 @@ -use super::GossipUi; +use super::{GossipUi, Page}; use crate::comms::BusMessage; use crate::db::DbPerson; use crate::globals::GLOBALS; @@ -63,6 +63,11 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra json_payload: serde_json::to_string(&pubkeyhex).unwrap(), }); } + + if ui.button("VIEW THEIR FEED").clicked() { + GLOBALS.feed.set_feed_to_person(pubkeyhex.clone()); + app.page = Page::FeedPerson; + } } } From d03cf859e1ea8bd8d502148c7648199c9d670298 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 3 Jan 2023 14:27:45 +1300 Subject: [PATCH 5/8] Remove globals.last_reply, we no longer need it --- src/globals.rs | 19 +------------------ src/process.rs | 12 ------------ 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/src/globals.rs b/src/globals.rs index b3f6ab10..1db9d829 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -7,7 +7,7 @@ use crate::people::People; use crate::relationship::Relationship; use crate::settings::Settings; use crate::signer::Signer; -use nostr_types::{Event, Id, IdHex, PublicKeyHex, Unixtime, Url}; +use nostr_types::{Event, Id, IdHex, PublicKeyHex, Url}; use rusqlite::Connection; use std::collections::{HashMap, HashSet}; use std::sync::atomic::AtomicBool; @@ -40,10 +40,6 @@ pub struct Globals { /// All relationships between events pub relationships: RwLock>>, - /// The date of the latest reply. Only reply relationships count, not reactions, - /// deletions, or quotes - pub last_reply: RwLock>, - /// Desired events, referred to by others, with possible URLs where we can /// get them. We may already have these, but if not we should ask for them. pub desired_events: RwLock>>, @@ -97,7 +93,6 @@ lazy_static! { events: RwLock::new(HashMap::new()), incoming_events: RwLock::new(Vec::new()), relationships: RwLock::new(HashMap::new()), - last_reply: RwLock::new(HashMap::new()), desired_events: RwLock::new(HashMap::new()), people: RwLock::new(People::new()), relays: RwLock::new(HashMap::new()), @@ -227,18 +222,6 @@ impl Globals { .or_insert_with(|| vec![r]); } - pub async fn update_last_reply(id: Id, time: Unixtime) { - let mut last_reply = GLOBALS.last_reply.write().await; - last_reply - .entry(id) - .and_modify(|lasttime| { - if time > *lasttime { - *lasttime = time; - } - }) - .or_insert_with(|| time); - } - pub fn get_replies_sync(id: Id) -> Vec { let mut output: Vec = Vec::new(); if let Some(vec) = GLOBALS.relationships.blocking_read().get(&id) { diff --git a/src/process.rs b/src/process.rs index f54fe362..1b4c10ff 100644 --- a/src/process.rs +++ b/src/process.rs @@ -141,18 +141,6 @@ pub async fn process_new_event( // Insert into relationships Globals::add_relationship(id, event.id, Relationship::Reply).await; - - // Update last_reply - let mut id = id; - Globals::update_last_reply(id, event.created_at).await; - while let Some(ev) = GLOBALS.events.read().await.get(&id).cloned() { - if let Some((pid, _)) = ev.replies_to() { - id = pid; - Globals::update_last_reply(id, event.created_at).await; - } else { - break; - } - } } // We desire all ancestors From 336db661ec7eb50d217ac3c1b4dd5cd3d4d58c19 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 3 Jan 2023 14:50:30 +1300 Subject: [PATCH 6/8] Rework subscriptions for these feeds (incomplete but it is generally working) --- src/feed.rs | 17 ++- src/overlord/minion/handle_bus.rs | 27 +++- src/overlord/minion/handle_websocket.rs | 2 +- src/overlord/minion/mod.rs | 186 +++++++++++++++++++++++- src/overlord/mod.rs | 4 +- 5 files changed, 216 insertions(+), 20 deletions(-) diff --git a/src/feed.rs b/src/feed.rs index 87e41514..5b84a066 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -1,3 +1,4 @@ +use crate::comms::BusMessage; use crate::globals::GLOBALS; use nostr_types::PublicKeyHex; use nostr_types::{Event, EventKind, Id}; @@ -38,16 +39,27 @@ impl Feed { } pub fn set_feed_to_general(&self) { + // We are always subscribed to the general feed. Don't resubscribe here + // because it won't have changed, but the relays will shower you with + // all those events again. *self.current_feed_kind.write() = FeedKind::General; } pub fn set_feed_to_thread(&self, id: Id) { - // get parent? + let _ = GLOBALS.to_minions.send(BusMessage { + target: "all".to_string(), + kind: "subscribe_thread_feed".to_string(), + json_payload: serde_json::to_string(&id).unwrap(), + }); *self.current_feed_kind.write() = FeedKind::Thread(id); } pub fn set_feed_to_person(&self, pubkey: PublicKeyHex) { - // FIXME - TRIGGER OVERLORD TO FETCH THEIR EVENTS FURTHER BACK + let _ = GLOBALS.to_minions.send(BusMessage { + target: "all".to_string(), + kind: "subscribe_person_feed".to_string(), + json_payload: serde_json::to_string(&pubkey).unwrap(), + }); *self.current_feed_kind.write() = FeedKind::Person(pubkey); } @@ -67,7 +79,6 @@ impl Feed { } pub fn get_thread_parent(&self, id: Id) -> Id { - // FIXME - TRIGGER OVERLORD TO FETCH THIS FEED let mut event = match GLOBALS.events.blocking_read().get(&id).cloned() { None => return id, Some(e) => e, diff --git a/src/overlord/minion/handle_bus.rs b/src/overlord/minion/handle_bus.rs index 5cf60422..70a7d4a3 100644 --- a/src/overlord/minion/handle_bus.rs +++ b/src/overlord/minion/handle_bus.rs @@ -1,18 +1,33 @@ use super::Minion; use crate::{BusMessage, Error}; use futures::SinkExt; -use nostr_types::{ClientMessage, Event, IdHex, PublicKeyHex}; +use nostr_types::{ClientMessage, Event, Id, IdHex, PublicKeyHex}; use tungstenite::protocol::Message as WsMessage; impl Minion { pub(super) async fn handle_bus_message( &mut self, bus_message: BusMessage, - ) -> Result<(), Error> { + ) -> Result { match &*bus_message.kind { - "set_followed_people" => { - let v: Vec = serde_json::from_str(&bus_message.json_payload)?; - self.upsert_following(v).await?; + "shutdown" => { + tracing::info!("{}: Websocket listener shutting down", &self.url); + return Ok(false); + } + //"set_followed_people" => { + // let v: Vec = serde_json::from_str(&bus_message.json_payload)?; + // self.upsert_following(v).await?; + //} + "subscribe_general_feed" => { + self.subscribe_general_feed().await?; + } + "subscribe_person_feed" => { + let pubkeyhex: PublicKeyHex = serde_json::from_str(&bus_message.json_payload)?; + self.subscribe_person_feed(pubkeyhex).await?; + } + "subscribe_thread_feed" => { + let id: Id = serde_json::from_str(&bus_message.json_payload)?; + self.subscribe_thread_feed(id).await?; } "fetch_events" => { let v: Vec = serde_json::from_str(&bus_message.json_payload)?; @@ -38,6 +53,6 @@ impl Minion { ); } } - Ok(()) + Ok(true) } } diff --git a/src/overlord/minion/handle_websocket.rs b/src/overlord/minion/handle_websocket.rs index c07fd78c..3e9d0a6f 100644 --- a/src/overlord/minion/handle_websocket.rs +++ b/src/overlord/minion/handle_websocket.rs @@ -29,7 +29,7 @@ impl Minion { .subscriptions .get_handle_by_id(&subid.0) .unwrap_or_else(|| "_".to_owned()); - tracing::trace!("{}: {}: NEW EVENT", &self.url, handle); + tracing::debug!("{}: {}: NEW EVENT", &self.url, handle); if event.kind == EventKind::TextNote { // Just store text notes in incoming diff --git a/src/overlord/minion/mod.rs b/src/overlord/minion/mod.rs index edb2294f..74240005 100644 --- a/src/overlord/minion/mod.rs +++ b/src/overlord/minion/mod.rs @@ -10,8 +10,9 @@ use futures::{SinkExt, StreamExt}; use futures_util::stream::{SplitSink, SplitStream}; use http::Uri; use nostr_types::{ - EventKind, Filter, IdHex, PublicKeyHex, RelayInformationDocument, Unixtime, Url, + EventKind, Filter, Id, IdHex, PublicKeyHex, RelayInformationDocument, Unixtime, Url, }; +use std::time::Duration; use subscription::Subscriptions; use tokio::net::TcpStream; use tokio::select; @@ -219,13 +220,8 @@ impl Minion { Err(e) => return Err(e.into()) }; #[allow(clippy::collapsible_if)] - if bus_message.target == self.url.inner() { - self.handle_bus_message(bus_message).await?; - } else if &*bus_message.target == "all" { - if &*bus_message.kind == "shutdown" { - tracing::info!("{}: Websocket listener shutting down", &self.url); - keepgoing = false; - } + if bus_message.target == self.url.inner() || bus_message.target == "all" { + keepgoing = self.handle_bus_message(bus_message).await?; } }, } @@ -243,7 +239,180 @@ impl Minion { Ok(()) } + async fn subscribe_general_feed(&mut self) -> Result<(), Error> { + // NOTE if the general feed is already subscribed we shoudn't do anything + // but we may need to update the subscription + + let mut filters: Vec = Vec::new(); + let feed_chunk = GLOBALS.settings.read().await.feed_chunk; + + let followed_pubkeys = GLOBALS.people.read().await.get_followed_pubkeys(); + + if let Some(pubkey) = GLOBALS.signer.read().await.public_key() { + // feed related by me + filters.push(Filter { + authors: vec![pubkey.into()], + kinds: vec![ + EventKind::TextNote, + EventKind::Reaction, + EventKind::EventDeletion, + ], + since: Some(Unixtime::now().unwrap() - Duration::from_secs(feed_chunk)), + ..Default::default() + }); + + // Any mentions of me + filters.push(Filter { + p: vec![pubkey.into()], + since: Some(Unixtime::now().unwrap() - Duration::from_secs(feed_chunk)), + ..Default::default() + }); + + // my metadata + // FIXME TBD + /* + filters.push(Filter { + authors: vec![pubkey], + kinds: vec![EventKind::Metadata, EventKind::RecommendRelay, EventKind::ContactList, EventKind::RelaysList], + since: // last we last checked + .. Default::default() + }); + */ + } + + if !followed_pubkeys.is_empty() { + // feed related by people followed + filters.push(Filter { + authors: followed_pubkeys.clone(), + kinds: vec![ + EventKind::TextNote, + EventKind::Reaction, + EventKind::EventDeletion, + ], + since: Some(Unixtime::now().unwrap() - Duration::from_secs(feed_chunk)), + ..Default::default() + }); + + // metadata by people followed + // FIXME TBD + /* + filters.push(Filter { + authors: pubkeys.clone(), + kinds: vec![EventKind::Metadata, EventKind::RecommendRelay, EventKind::ContactList, EventKind::RelaysList], + since: // last we last checked + .. Default::default() + }); + */ + } + + // reactions to posts by me + // FIXME TBD + + // reactions to posts by people followed + // FIXME TBD + + // NO REPLIES OR ANCESTORS + + if filters.is_empty() { + self.unsubscribe("general_feed").await?; + } else { + self.subscribe(filters, "general_feed").await?; + } + + Ok(()) + } + + async fn subscribe_person_feed(&mut self, pubkey: PublicKeyHex) -> Result<(), Error> { + // NOTE we do not unsubscribe to the general feed + + let mut filters: Vec = Vec::new(); + let feed_chunk = GLOBALS.settings.read().await.feed_chunk; + + // feed related by person + filters.push(Filter { + authors: vec![pubkey], + kinds: vec![ + EventKind::TextNote, + EventKind::Reaction, + EventKind::EventDeletion, + ], + since: Some(Unixtime::now().unwrap() - Duration::from_secs(feed_chunk)), + ..Default::default() + }); + + // persons metadata + // FIXME TBD + /* + filters.push(Filter { + authors: vec![pubkey], + kinds: vec![EventKind::Metadata, EventKind::RecommendRelay, EventKind::ContactList, EventKind::RelaysList], + since: // last we last checked + .. Default::default() + }); + */ + + // reactions to post by person + // FIXME TBD + + // NO REPLIES OR ANCESTORS + + if filters.is_empty() { + self.unsubscribe("person_feed").await?; + } else { + self.subscribe(filters, "person_feed").await?; + } + + Ok(()) + } + + async fn subscribe_thread_feed(&mut self, id: Id) -> Result<(), Error> { + // NOTE we do not unsubscribe to the general feed + + let mut filters: Vec = Vec::new(); + let feed_chunk = GLOBALS.settings.read().await.feed_chunk; + + // This post and ancestors + let mut ids: Vec = vec![id.into()]; + // FIXME - We could have this precalculated like GLOBALS.relationships + // in reverse. It would be potentially more complete having + // iteratively climbed the chain. + if let Some(event) = GLOBALS.events.read().await.get(&id) { + for (id, url) in &event.replies_to_ancestors() { + if let Some(url) = url { + if url == &self.url { + ids.push((*id).into()); + } + } else { + ids.push((*id).into()); + } + } + } + filters.push(Filter { + ids: ids.clone(), + ..Default::default() + }); + + // Replies and reactions to this post and ancestors + filters.push(Filter { + e: ids, + since: Some(Unixtime::now().unwrap() - Duration::from_secs(feed_chunk)), + ..Default::default() + }); + + // Metadata for people in those events + // TBD + + if filters.is_empty() { + self.unsubscribe("thread_feed").await?; + } else { + self.subscribe(filters, "thread_feed").await?; + } + + Ok(()) + } + // Create or replace the following subscription + /* async fn upsert_following(&mut self, pubkeys: Vec) -> Result<(), Error> { let websocket_sink = self.sink.as_mut().unwrap(); @@ -354,6 +523,7 @@ impl Minion { Ok(()) } + */ async fn get_events(&mut self, ids: Vec) -> Result<(), Error> { if ids.is_empty() { diff --git a/src/overlord/mod.rs b/src/overlord/mod.rs index a7abbaac..930f8725 100644 --- a/src/overlord/mod.rs +++ b/src/overlord/mod.rs @@ -220,10 +220,10 @@ impl Overlord { // Fire off a minion to handle this relay self.start_minion(best_relay.relay.url.clone()).await?; - // Tell it to follow the chosen people + // Subscribe to the general feed let _ = self.to_minions.send(BusMessage { target: best_relay.relay.url.clone(), - kind: "set_followed_people".to_string(), + kind: "subscribe_general_feed".to_string(), json_payload: serde_json::to_string(&best_relay.pubkeys).unwrap(), }); From 341af37e7d5bc80fdccdf5898c2464a8d324250f Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 3 Jan 2023 17:18:37 +1300 Subject: [PATCH 7/8] Go back to processing everything immediately, see how it goes --- src/overlord/minion/handle_websocket.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/overlord/minion/handle_websocket.rs b/src/overlord/minion/handle_websocket.rs index 3e9d0a6f..7c36d4b1 100644 --- a/src/overlord/minion/handle_websocket.rs +++ b/src/overlord/minion/handle_websocket.rs @@ -31,6 +31,11 @@ impl Minion { .unwrap_or_else(|| "_".to_owned()); tracing::debug!("{}: {}: NEW EVENT", &self.url, handle); + // Try processing everything immediately + crate::process::process_new_event(&event, true, Some(self.url.clone())) + .await?; + + /* if event.kind == EventKind::TextNote { // Just store text notes in incoming GLOBALS @@ -43,6 +48,8 @@ impl Minion { crate::process::process_new_event(&event, true, Some(self.url.clone())) .await?; } + */ + } } RelayMessage::Notice(msg) => { From 3a767f05fc1a855a79ee077162842c587f7046fc Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 3 Jan 2023 17:29:15 +1300 Subject: [PATCH 8/8] Fix following feed pollution --- src/feed.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/feed.rs b/src/feed.rs index 5b84a066..6b0fe9a1 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -146,6 +146,11 @@ impl Feed { .map(|e| e.to_owned()) .collect(); + let mut pubkeys = GLOBALS.people.blocking_read().get_followed_pubkeys(); + if let Some(pubkey) = GLOBALS.signer.blocking_read().public_key() { + pubkeys.push(pubkey.into()); // add the user + } + // My event ids if let Some(pubkey) = GLOBALS.signer.blocking_read().public_key() { *self.my_event_ids.write() = events @@ -157,11 +162,10 @@ impl Feed { } // Followed event ids - let followed_pubkeys = GLOBALS.people.blocking_read().get_followed_pubkeys(); *self.followed_event_ids.write() = events .iter() .filter_map(|e| { - if followed_pubkeys.contains(&e.pubkey.into()) { + if pubkeys.contains(&e.pubkey.into()) { Some(e.id) } else { None @@ -172,11 +176,13 @@ impl Feed { // Filter further for the feed let mut events: Vec = events .iter() + .filter(|e| pubkeys.contains(&e.pubkey.into())) // something we follow .filter(|e| !GLOBALS.dismissed.blocking_read().contains(&e.id)) .cloned() .collect(); - events.sort_unstable_by(|a, b| b.created_at.cmp(&a.created_at)); + // In time order + events.sort_by(|a, b| b.created_at.cmp(&a.created_at)); *self.general_feed.write() = events.iter().map(|e| e.id).collect(); }