From 0d96d8ab5f92305bdfd2ec18b41d240eeaf061ce Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 17 Mar 2024 05:53:42 +1300 Subject: [PATCH 01/29] Disable pending thread until UI is ready to implement --- gossip-lib/src/lib.rs | 3 ++- gossip-lib/src/pending.rs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gossip-lib/src/lib.rs b/gossip-lib/src/lib.rs index 58e1cf5a..f0163b9f 100644 --- a/gossip-lib/src/lib.rs +++ b/gossip-lib/src/lib.rs @@ -244,7 +244,8 @@ pub async fn run() { crate::people::People::start(); // Start periodic tasks in pending - crate::pending::start(); + // DISABLED until the UI is ready to implement. + // crate::pending::start(); // Start long-lived subscriptions // (this also does a relay_picker init) diff --git a/gossip-lib/src/pending.rs b/gossip-lib/src/pending.rs index ec5f78a4..dfc37e8c 100644 --- a/gossip-lib/src/pending.rs +++ b/gossip-lib/src/pending.rs @@ -78,6 +78,7 @@ impl Pending { } } +#[allow(dead_code)] pub fn start() { task::spawn(async { let mut read_runstate = GLOBALS.read_runstate.clone(); From 4d0f5f9d730349fb3f0b2453ff99d4cbc6228c33 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 17 Mar 2024 06:09:08 +1300 Subject: [PATCH 02/29] Fix force of purplepag.es fixes #627 --- gossip-bin/src/ui/mod.rs | 1 + gossip-bin/src/ui/wizard/setup_relays.rs | 4 ++-- gossip-bin/src/ui/wizard/wizard_state.rs | 12 +++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index bbc53afd..c05eeb8c 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -629,6 +629,7 @@ impl GossipUi { let mut wizard_state: WizardState = Default::default(); let wizard_complete = GLOBALS.storage.get_flag_wizard_complete(); if !wizard_complete { + wizard_state.init(); if let Some(wp) = wizard::start_wizard_page(&mut wizard_state) { start_page = Page::Wizard(wp); } diff --git a/gossip-bin/src/ui/wizard/setup_relays.rs b/gossip-bin/src/ui/wizard/setup_relays.rs index d8eed126..0eb3667d 100644 --- a/gossip-bin/src/ui/wizard/setup_relays.rs +++ b/gossip-bin/src/ui/wizard/setup_relays.rs @@ -14,7 +14,7 @@ use std::collections::BTreeMap; const MIN_OUTBOX: usize = 3; const MIN_INBOX: usize = 2; -const MIN_DISCOVERY: usize = 1; +const MIN_DISCOVERY: usize = 4; pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { let read_relay = |url: &RelayUrl| GLOBALS.storage.read_or_create_relay(url, None).unwrap(); @@ -135,7 +135,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.label(RichText::new(" - OK").color(Color32::GREEN)); } else { ui.label( - RichText::new(" - You should have one") + RichText::new(" - We suggest 4") .color(app.theme.warning_marker_text_color()), ); } diff --git a/gossip-bin/src/ui/wizard/wizard_state.rs b/gossip-bin/src/ui/wizard/wizard_state.rs index e02839cf..0cb2ee9a 100644 --- a/gossip-bin/src/ui/wizard/wizard_state.rs +++ b/gossip-bin/src/ui/wizard/wizard_state.rs @@ -76,6 +76,13 @@ impl Default for WizardState { } } impl WizardState { + pub fn init(&mut self) { + if self.need_discovery_relays() { + let purplepages = RelayUrl::try_from_str("wss://purplepag.es/").unwrap(); + super::modify_relay(&purplepages, |relay| relay.set_usage_bits(Relay::DISCOVER)); + } + } + pub fn update(&mut self) { self.follow_only = GLOBALS.storage.get_flag_following_only(); @@ -112,11 +119,6 @@ impl WizardState { .map(|(pk, _)| (Some(pk), None)) .collect(); - if self.need_discovery_relays() { - let purplepages = RelayUrl::try_from_str("wss://purplepag.es/").unwrap(); - super::modify_relay(&purplepages, |relay| relay.set_usage_bits(Relay::DISCOVER)); - } - // Copy any new status queue messages into our local error variable let last_status_queue_message = GLOBALS.status_queue.read().read_last(); if last_status_queue_message != self.last_status_queue_message { From 832e314b18f4e7025b884b2c8df802d05f9ee9bb Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 17 Mar 2024 07:09:02 +1300 Subject: [PATCH 03/29] gossip-lib/misc.rs module --- gossip-lib/src/globals.rs | 15 ++------------- gossip-lib/src/lib.rs | 5 ++++- gossip-lib/src/misc.rs | 11 +++++++++++ gossip-lib/src/overlord/mod.rs | 3 ++- 4 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 gossip-lib/src/misc.rs diff --git a/gossip-lib/src/globals.rs b/gossip-lib/src/globals.rs index d5b28db8..e60f3e89 100644 --- a/gossip-lib/src/globals.rs +++ b/gossip-lib/src/globals.rs @@ -5,6 +5,7 @@ use crate::feed::Feed; use crate::fetcher::Fetcher; use crate::gossip_identity::GossipIdentity; use crate::media::Media; +use crate::misc::ZapState; use crate::nip46::ParsedCommand; use crate::pending::Pending; use crate::people::{People, Person}; @@ -15,9 +16,7 @@ use crate::storage::Storage; use crate::RunState; use dashmap::{DashMap, DashSet}; use gossip_relay_picker::RelayPicker; -use nostr_types::{ - Event, Id, PayRequestData, Profile, PublicKey, RelayUrl, RelayUsage, UncheckedUrl, -}; +use nostr_types::{Event, Id, Profile, PublicKey, RelayUrl, RelayUsage}; use parking_lot::RwLock as PRwLock; use regex::Regex; use rhai::{Engine, AST}; @@ -28,16 +27,6 @@ use tokio::sync::watch::Receiver as WatchReceiver; use tokio::sync::watch::Sender as WatchSender; use tokio::sync::{broadcast, mpsc, Mutex, Notify, RwLock}; -/// The state that a Zap is in (it moves through 5 states before it is complete) -#[derive(Debug, Clone)] -pub enum ZapState { - None, - CheckingLnurl(Id, PublicKey, UncheckedUrl), - SeekingAmount(Id, PublicKey, PayRequestData, UncheckedUrl), - LoadingInvoice(Id, PublicKey), - ReadyToPay(Id, String), // String is the Zap Invoice as a string, to be shown as a QR code -} - /// Global data shared between threads. Access via the static ref `GLOBALS`. pub struct Globals { /// This is a broadcast channel. All Minions should listen on it. diff --git a/gossip-lib/src/lib.rs b/gossip-lib/src/lib.rs index f0163b9f..d24c3e29 100644 --- a/gossip-lib/src/lib.rs +++ b/gossip-lib/src/lib.rs @@ -90,7 +90,7 @@ pub use fetcher::Fetcher; mod filter; mod globals; -pub use globals::{Globals, ZapState, GLOBALS}; +pub use globals::{Globals, GLOBALS}; mod gossip_identity; pub use gossip_identity::GossipIdentity; @@ -98,6 +98,9 @@ pub use gossip_identity::GossipIdentity; mod media; pub use media::Media; +mod misc; +pub use misc::ZapState; + /// Rendering various names of users pub mod names; diff --git a/gossip-lib/src/misc.rs b/gossip-lib/src/misc.rs new file mode 100644 index 00000000..97de765b --- /dev/null +++ b/gossip-lib/src/misc.rs @@ -0,0 +1,11 @@ +use nostr_types::{Id, PayRequestData, PublicKey, UncheckedUrl}; + +/// The state that a Zap is in (it moves through 5 states before it is complete) +#[derive(Debug, Clone)] +pub enum ZapState { + None, + CheckingLnurl(Id, PublicKey, UncheckedUrl), + SeekingAmount(Id, PublicKey, PayRequestData, UncheckedUrl), + LoadingInvoice(Id, PublicKey), + ReadyToPay(Id, String), // String is the Zap Invoice as a string, to be shown as a QR code +} diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 24ffc3db..6b77c7ea 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -7,7 +7,8 @@ use crate::comms::{ use crate::dm_channel::DmChannel; use crate::error::{Error, ErrorKind}; use crate::feed::FeedKind; -use crate::globals::{Globals, ZapState, GLOBALS}; +use crate::globals::{Globals, GLOBALS}; +use crate::misc::ZapState; use crate::nip46::{Approval, ParsedCommand}; use crate::people::{Person, PersonList}; use crate::person_relay::PersonRelay; From 92b547a43d78905be456ccb5bd929b76d16a09a3 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 18 Mar 2024 12:31:48 +1300 Subject: [PATCH 04/29] Changes for building AppImage --- gossip-bin/Cargo.toml | 1 + gossip-lib/Cargo.toml | 3 +++ gossip-lib/src/profile.rs | 7 ++++++ packaging/appimage/build-appimage.sh | 36 ++++++++++++++++++++++++---- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/gossip-bin/Cargo.toml b/gossip-bin/Cargo.toml index 60afba44..280abb81 100644 --- a/gossip-bin/Cargo.toml +++ b/gossip-bin/Cargo.toml @@ -16,6 +16,7 @@ video-ffmpeg = [ "egui-video", "sdl2" ] native-tls = [ "gossip-lib/native-tls" ] rustls-tls = [ "gossip-lib/rustls-tls" ] rustls-tls-native = [ "gossip-lib/rustls-tls-native" ] +appimage = [ "gossip-lib/appimage" ] [dependencies] bech32 = "0.9" diff --git a/gossip-lib/Cargo.toml b/gossip-lib/Cargo.toml index 6d110f1e..06af703f 100644 --- a/gossip-lib/Cargo.toml +++ b/gossip-lib/Cargo.toml @@ -35,6 +35,9 @@ rustls-tls-native = [ "tokio-tungstenite/rustls-tls-native-roots" ] +# Make tweaks for AppImage +appimage = [] + [dependencies] async-recursion = "1.0" async-trait = "0.1" diff --git a/gossip-lib/src/profile.rs b/gossip-lib/src/profile.rs index 824c33b1..031ccda6 100644 --- a/gossip-lib/src/profile.rs +++ b/gossip-lib/src/profile.rs @@ -30,6 +30,13 @@ pub struct Profile { impl Profile { fn new() -> Result { + if cfg!(feature = "appimage") { + // Because AppImage only changes $HOME (and not $XDG_DATA_HOME), we unset + // $XDG_DATA_HOME and let it use the changed $HOME on linux to find the + // data directory + std::env::remove_var("XDG_DATA_HOME"); + } + // Get system standard directory for user data let data_dir = dirs::data_dir() .ok_or::("Cannot find a directory to store application data.".into())?; diff --git a/packaging/appimage/build-appimage.sh b/packaging/appimage/build-appimage.sh index ea82c327..77e4a3c1 100755 --- a/packaging/appimage/build-appimage.sh +++ b/packaging/appimage/build-appimage.sh @@ -18,18 +18,44 @@ trap cleanup EXIT cd ../.. RUSTFLAGS="-C target-cpu=native --cfg tokio_unstable" -cargo build --features=lang-cjk --release +cargo build --features=lang-cjk,appimage --release cd target/ rm -rf ./appimage mkdir -p appimage cd appimage/ -wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -chmod +x linuxdeploy-x86_64.AppImage +# Build AppDir +mkdir -p AppDir/usr/bin/ +mkdir -p AppDir/usr/lib/ +mkdir -p AppDir/usr/share/applications/ +mkdir -p AppDir/usr/share/icons/hicolor/scalable/apps/ +mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps/ +mkdir -p AppDir/usr/share/icons/hicolor/128x128/apps/ +mkdir -p AppDir/usr/share/icons/hicolor/64x64/apps/ +mkdir -p AppDir/usr/share/icons/hicolor/32x32/apps/ +mkdir -p AppDir/usr/share/icons/hicolor/16x16/apps/ +cp ../release/gossip AppDir/usr/bin/gossip +strip AppDir/usr/bin/gossip +cp ../../logo/gossip.png AppDir/usr/share/icons/hicolor/128x128/apps/gossip.png +cp ../../logo/gossip.svg AppDir/usr/share/icons/hicolor/scalable/apps/gossip.svg +cp ../../packaging/debian/gossip.desktop AppDir/usr/share/applications/gossip.desktop +ln -s usr/bin/gossip AppDir/AppRun +ln -s gossip.png AppDir/.DirIcon +ln -s usr/share/applications/gossip.desktop AppDir/gossip.desktop +ln -s usr/share/icons/hicolor/128x128/apps/gossip.png AppDir/gossip.png -./linuxdeploy-x86_64.AppImage --appdir AppDir -e ../release/gossip -i ../../logo/gossip.png -d ../../packaging/debian/gossip.desktop --output=appimage +# Get appimagetool +wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" +chmod a+x appimagetool-x86_64.AppImage -echo "AppImage is at ../../target/appimage/gossip-x86_64.AppImage" +# Use appimagetool to build the AppImage +./appimagetool-x86_64.AppImage AppDir + +# Bundle for portable mode +mkdir -p gossip-x86_64.AppImage.home/.local/share +tar cvf - gossip-x86_64.AppImage gossip-x86_64.AppImage.home | gzip -c > gossip-x86_64.AppImage.tar.gz + +echo "Portable AppImage is at ../../target/appimage/gossip-x86_64.AppImage.tar.gz" exit 0 From d33f3ac8cddc5793ec9768b553b5c47fa17c852a Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Mon, 18 Mar 2024 09:54:28 -0600 Subject: [PATCH 05/29] Thread: Auto-scroll to note only when note is not already in view to avoid UI "jitters" --- gossip-bin/src/ui/feed/note/mod.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gossip-bin/src/ui/feed/note/mod.rs b/gossip-bin/src/ui/feed/note/mod.rs index dfb57228..21a51365 100644 --- a/gossip-bin/src/ui/feed/note/mod.rs +++ b/gossip-bin/src/ui/feed/note/mod.rs @@ -153,11 +153,14 @@ pub(super) fn render_note( // scroll to this note if it's the main note of a thread and the user hasn't scrolled yet if is_main_event && app.feeds.thread_needs_scroll { - // keep auto-scrolling untill user scrolls + // keep auto-scrolling until user scrolls if app.current_scroll_offset != 0.0 { app.feeds.thread_needs_scroll = false; } - inner_response.response.scroll_to_me(Some(Align::Center)); + // only request scrolling if the note is not completely visible + if !ui.clip_rect().contains_rect(inner_response.response.rect) { + inner_response.response.scroll_to_me(Some(Align::Center)); + } } // Mark post as viewed if hovered AND we are not scrolling From 11a12729c0820e83d7614b3185174d9374668105 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 19 Mar 2024 10:22:05 +1300 Subject: [PATCH 06/29] comment --- gossip-lib/src/overlord/minion/filter_fns.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gossip-lib/src/overlord/minion/filter_fns.rs b/gossip-lib/src/overlord/minion/filter_fns.rs index 9a412d88..aff19d16 100644 --- a/gossip-lib/src/overlord/minion/filter_fns.rs +++ b/gossip-lib/src/overlord/minion/filter_fns.rs @@ -176,6 +176,8 @@ pub fn outbox(since: Unixtime) -> Vec { } } +// This FORCES the fetch of relay lists without checking if we need them. +// See also relay_lists() which checks if they are needed first. pub fn discover(pubkeys: &[PublicKey]) -> Vec { let pkp: Vec = pubkeys.iter().map(|pk| pk.into()).collect(); vec![Filter { From 7857695b75c5eb086999ec26f7181a4cd40c4993 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 19 Mar 2024 11:26:55 +1300 Subject: [PATCH 07/29] Fix our update of person.relay_list_last_received (we weren't saving the date if it was stale) --- gossip-lib/src/storage/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index 36a38f7c..e67f4b03 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -1163,16 +1163,20 @@ impl Storage { pub fn process_relay_list(&self, event: &Event) -> Result<(), Error> { let mut txn = self.env.write_txn()?; - // Check if this relay list is newer than the stamp we have for its author if let Some(mut person) = self.read_person(&event.pubkey)? { - // Mark that we received it (changes fetch duration for next time) + // Mark that we received it. + // This prevents us from refetching again and again. person.relay_list_last_received = Unixtime::now().unwrap().0; + // Check if this relay list is newer than the stamp we have for its author if let Some(previous_at) = person.relay_list_created_at { if event.created_at.0 <= previous_at { + // This list is old. But let's save the last_received setting: + self.write_person(&person, Some(&mut txn))?; return Ok(()); } } + // If we got here, the list is new. // Mark when it was created person.relay_list_created_at = Some(event.created_at.0); From d0e0c6a4c3c3671045ac640441b57d4db6aa112b Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 17 Mar 2024 09:09:45 +1300 Subject: [PATCH 08/29] overlord: FetchEvent: use READ relays if none are specified --- gossip-lib/src/overlord/mod.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 6b77c7ea..a89976e9 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -1320,7 +1320,21 @@ impl Overlord { } /// Fetch an event from a specific relay by event `Id` - pub async fn fetch_event(&mut self, id: Id, relay_urls: Vec) -> Result<(), Error> { + pub async fn fetch_event( + &mut self, + id: Id, + mut relay_urls: Vec, + ) -> Result<(), Error> { + // Use READ relays if relays are unknown + if relay_urls.is_empty() { + relay_urls = GLOBALS + .storage + .filter_relays(|r| r.has_usage_bits(Relay::READ) && r.rank != 0)? + .iter() + .map(|relay| relay.url.clone()) + .collect(); + } + // Don't do this if we already have the event if !GLOBALS.storage.has_event(id)? { // Note: minions will remember if they get the same id multiple times From 302750ff2ab49693cb4688a03cfa2ac7e100a145 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 20 Mar 2024 11:36:37 +1300 Subject: [PATCH 09/29] downgrade a log msg --- gossip-lib/src/overlord/minion/handle_websocket.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gossip-lib/src/overlord/minion/handle_websocket.rs b/gossip-lib/src/overlord/minion/handle_websocket.rs index 23929633..b590e0c1 100644 --- a/gossip-lib/src/overlord/minion/handle_websocket.rs +++ b/gossip-lib/src/overlord/minion/handle_websocket.rs @@ -40,7 +40,7 @@ impl Minion { } } if !it_matches { - tracing::info!( + tracing::debug!( "{} sent event that does not match filters on subscription {}: {}", self.url, handle, From 41baa88d057e909ea3d763796ae5764009739374 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 20 Mar 2024 12:00:05 +1300 Subject: [PATCH 10/29] command: backdate_eose() --- gossip-bin/src/commands.rs | 26 +++++++++++++++++++++++++- gossip-lib/src/storage/mod.rs | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/gossip-bin/src/commands.rs b/gossip-bin/src/commands.rs index 6ffc297a..e6a0c7ac 100644 --- a/gossip-bin/src/commands.rs +++ b/gossip-bin/src/commands.rs @@ -29,7 +29,7 @@ impl Command { } } -const COMMANDS: [Command; 30] = [ +const COMMANDS: [Command; 31] = [ Command { cmd: "oneshot", usage_params: "{depends}", @@ -45,6 +45,11 @@ const COMMANDS: [Command; 30] = [ usage_params: "", desc: "add a new person list with the given name", }, + Command { + cmd: "backdate_eose", + usage_params: "", + desc: "backdate last_general_eose_at by 24 hours for every relay", + }, Command { cmd: "bech32_decode", usage_params: "", @@ -202,6 +207,7 @@ pub fn handle_command(mut args: env::Args, runtime: &Runtime) -> Result oneshot(command, args)?, "add_person_relay" => add_person_relay(command, args)?, "add_person_list" => add_person_list(command, args)?, + "backdate_eose" => backdate_eose()?, "bech32_decode" => bech32_decode(command, args)?, "bech32_encode_event_addr" => bech32_encode_event_addr(command, args)?, "decrypt" => decrypt(command, args)?, @@ -307,6 +313,24 @@ pub fn add_person_list(cmd: Command, mut args: env::Args) -> Result<(), Error> { Ok(()) } +pub fn backdate_eose() -> Result<(), Error> { + let now = Unixtime::now().unwrap(); + let ago = (now.0 - 60 * 60 * 24) as u64; + + GLOBALS.storage.modify_all_relays( + |relay| { + if let Some(eose) = relay.last_general_eose_at { + if eose > ago { + relay.last_general_eose_at = Some(ago); + } + } + }, + None, + )?; + + Ok(()) +} + pub fn bech32_decode(cmd: Command, mut args: env::Args) -> Result<(), Error> { let mut param = match args.next() { Some(s) => s, diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index e67f4b03..42f1ed74 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -1100,7 +1100,7 @@ impl Storage { //// Modify all relay records #[inline] - pub(crate) fn modify_all_relays<'a, M>( + pub fn modify_all_relays<'a, M>( &'a self, modify: M, rw_txn: Option<&mut RwTxn<'a>>, From c43135d2bf1d7f24f416e3fc71a66bbe6cc42e7e Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 18 Mar 2024 09:05:42 +1300 Subject: [PATCH 11/29] Option to render feed with newest at bottom --- gossip-bin/src/ui/feed/mod.rs | 125 +++++++++++++++++------------ gossip-bin/src/ui/settings/ui.rs | 4 + gossip-bin/src/unsaved_settings.rs | 4 + gossip-lib/src/storage/mod.rs | 1 + 4 files changed, 81 insertions(+), 53 deletions(-) diff --git a/gossip-bin/src/ui/feed/mod.rs b/gossip-bin/src/ui/feed/mod.rs index a0c81de7..b0f786ad 100644 --- a/gossip-bin/src/ui/feed/mod.rs +++ b/gossip-bin/src/ui/feed/mod.rs @@ -256,7 +256,10 @@ fn render_a_feed( is_thread: threaded, }; + let feed_newest_at_bottom = GLOBALS.storage.read_setting_feed_newest_at_bottom(); + app.vert_scroll_area() + .stick_to_bottom(feed_newest_at_bottom) .id_source(scroll_area_id) .show(ui, |ui| { egui::Frame::none() @@ -264,66 +267,82 @@ fn render_a_feed( .fill(app.theme.feed_scroll_fill(&feed_properties)) .stroke(app.theme.feed_scroll_stroke(&feed_properties)) .show(ui, |ui| { - let iter = feed.iter(); - let first = feed.first(); - let last = feed.last(); - for id in iter { - render_note_maybe_fake( - app, - ctx, - ui, - FeedNoteParams { - id: *id, - indent: 0, - as_reply_to: false, - threaded, - is_first: Some(id) == first, - is_last: Some(id) == last, - }, - ); - } - - let recomputing = GLOBALS - .feed - .recompute_lock - .load(std::sync::atomic::Ordering::Relaxed); - - if !recomputing && offer_load_more { + if feed_newest_at_bottom { + ui.add_space(50.0); + if offer_load_more { + render_load_more(app, ui) + } ui.add_space(50.0); - ui.with_layout( - egui::Layout::top_down(egui::Align::Center) - .with_cross_align(egui::Align::Center), - |ui| { - app.theme.accent_button_1_style(ui.style_mut()); - ui.spacing_mut().button_padding.x *= 3.0; - ui.spacing_mut().button_padding.y *= 2.0; - let response = ui.add(egui::Button::new("Load More")); - if response.clicked() { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::LoadMoreCurrentFeed); - } - // draw some nice lines left and right of the button - let stroke = egui::Stroke::new(1.5, ui.visuals().extreme_bg_color); - let width = - (ui.available_width() - response.rect.width()) / 2.0 - 20.0; - let left_start = - response.rect.left_center() - egui::vec2(10.0, 0.0); - let left_end = left_start - egui::vec2(width, 0.0); - ui.painter().line_segment([left_start, left_end], stroke); - let right_start = - response.rect.right_center() + egui::vec2(10.0, 0.0); - let right_end = right_start + egui::vec2(width, 0.0); - ui.painter().line_segment([right_start, right_end], stroke); - }, - ); + for id in feed.iter().rev() { + render_note_maybe_fake( + app, + ctx, + ui, + FeedNoteParams { + id: *id, + indent: 0, + as_reply_to: false, + threaded, + is_first: Some(id) == feed.last(), + is_last: Some(id) == feed.first(), + }, + ); + } + } else { + for id in feed.iter() { + render_note_maybe_fake( + app, + ctx, + ui, + FeedNoteParams { + id: *id, + indent: 0, + as_reply_to: false, + threaded, + is_first: Some(id) == feed.first(), + is_last: Some(id) == feed.last(), + }, + ); + } + + ui.add_space(50.0); + if offer_load_more { + render_load_more(app, ui) + } + ui.add_space(50.0); } - ui.add_space(100.0); }); }); } +fn render_load_more(app: &mut GossipUi, ui: &mut Ui) { + ui.with_layout( + egui::Layout::top_down(egui::Align::Center).with_cross_align(egui::Align::Center), + |ui| { + app.theme.accent_button_1_style(ui.style_mut()); + ui.spacing_mut().button_padding.x *= 3.0; + ui.spacing_mut().button_padding.y *= 2.0; + let response = ui.add(egui::Button::new("Load More")); + if response.clicked() { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::LoadMoreCurrentFeed); + } + + // draw some nice lines left and right of the button + let stroke = egui::Stroke::new(1.5, ui.visuals().extreme_bg_color); + let width = (ui.available_width() - response.rect.width()) / 2.0 - 20.0; + let left_start = response.rect.left_center() - egui::vec2(10.0, 0.0); + let left_end = left_start - egui::vec2(width, 0.0); + ui.painter().line_segment([left_start, left_end], stroke); + let right_start = response.rect.right_center() + egui::vec2(10.0, 0.0); + let right_end = right_start + egui::vec2(width, 0.0); + ui.painter().line_segment([right_start, right_end], stroke); + }, + ); +} + fn render_note_maybe_fake( app: &mut GossipUi, ctx: &Context, diff --git a/gossip-bin/src/ui/settings/ui.rs b/gossip-bin/src/ui/settings/ui.rs index be2abbfa..5415343a 100644 --- a/gossip-bin/src/ui/settings/ui.rs +++ b/gossip-bin/src/ui/settings/ui.rs @@ -15,6 +15,10 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra &mut app.unsaved_settings.posting_area_at_top, "Show posting area at the top instead of the bottom", ); + ui.checkbox( + &mut app.unsaved_settings.feed_newest_at_bottom, + "Order feed with newest at bottom (intead of top)", + ); ui.add_space(20.0); ui.horizontal(|ui| { diff --git a/gossip-bin/src/unsaved_settings.rs b/gossip-bin/src/unsaved_settings.rs index 80531c1a..3929707a 100644 --- a/gossip-bin/src/unsaved_settings.rs +++ b/gossip-bin/src/unsaved_settings.rs @@ -89,6 +89,7 @@ pub struct UnsavedSettings { pub follow_os_dark_mode: bool, pub override_dpi: Option, pub highlight_unread_events: bool, + pub feed_newest_at_bottom: bool, pub posting_area_at_top: bool, pub status_bar: bool, pub image_resize_algorithm: String, @@ -172,6 +173,7 @@ impl Default for UnsavedSettings { follow_os_dark_mode: default_setting!(follow_os_dark_mode), override_dpi: default_setting!(override_dpi), highlight_unread_events: default_setting!(highlight_unread_events), + feed_newest_at_bottom: default_setting!(feed_newest_at_bottom), posting_area_at_top: default_setting!(posting_area_at_top), status_bar: default_setting!(status_bar), image_resize_algorithm: default_setting!(image_resize_algorithm), @@ -257,6 +259,7 @@ impl UnsavedSettings { follow_os_dark_mode: load_setting!(follow_os_dark_mode), override_dpi: load_setting!(override_dpi), highlight_unread_events: load_setting!(highlight_unread_events), + feed_newest_at_bottom: load_setting!(feed_newest_at_bottom), posting_area_at_top: load_setting!(posting_area_at_top), status_bar: load_setting!(status_bar), image_resize_algorithm: load_setting!(image_resize_algorithm), @@ -338,6 +341,7 @@ impl UnsavedSettings { save_setting!(follow_os_dark_mode, self, txn); save_setting!(override_dpi, self, txn); save_setting!(highlight_unread_events, self, txn); + save_setting!(feed_newest_at_bottom, self, txn); save_setting!(posting_area_at_top, self, txn); save_setting!(status_bar, self, txn); save_setting!(image_resize_algorithm, self, txn); diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index 42f1ed74..4f45bdb2 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -791,6 +791,7 @@ impl Storage { bool, true ); + def_setting!(feed_newest_at_bottom, b"feed_newest_at_bottom", bool, false); def_setting!(posting_area_at_top, b"posting_area_at_top", bool, true); def_setting!(status_bar, b"status_bar", bool, false); def_setting!( From 20a6d49d2daba54f08ece0bcf07e621fa11e5560 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 18 Mar 2024 09:35:48 +1300 Subject: [PATCH 12/29] Give the post icon some opacity so we can see what is in the post behind it, and move to top if feed reversed --- gossip-bin/src/ui/mod.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index c05eeb8c..e19b4b8d 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -1173,9 +1173,20 @@ impl GossipUi { // ---- "plus icon" ---- if !self.show_post_area_fn() && self.page.show_post_icon() { - let bottom_right = ui.ctx().screen_rect().right_bottom(); - let pos = bottom_right - + Vec2::new(-crate::AVATAR_SIZE_F32 * 2.0, -crate::AVATAR_SIZE_F32 * 2.0); + let feed_newest_at_bottom = + GLOBALS.storage.read_setting_feed_newest_at_bottom(); + let pos = if feed_newest_at_bottom { + let top_right = ui.ctx().screen_rect().right_top(); + top_right + + Vec2::new(-crate::AVATAR_SIZE_F32 * 2.0, crate::AVATAR_SIZE_F32 * 2.0) + } else { + let bottom_right = ui.ctx().screen_rect().right_bottom(); + bottom_right + + Vec2::new( + -crate::AVATAR_SIZE_F32 * 2.0, + -crate::AVATAR_SIZE_F32 * 2.0, + ) + }; egui::Area::new(ui.next_auto_id()) .movable(false) @@ -1195,6 +1206,15 @@ impl GossipUi { } else { RichText::new("\u{1f513}").size(20.0) }; + let fill_color = { + let fill_color_tuple = self.theme.accent_color().to_tuple(); + Color32::from_rgba_premultiplied( + fill_color_tuple.0, + fill_color_tuple.1, + fill_color_tuple.2, + 128, // half transparent + ) + }; let response = ui.add_sized( [crate::AVATAR_SIZE_F32, crate::AVATAR_SIZE_F32], egui::Button::new( @@ -1202,7 +1222,7 @@ impl GossipUi { ) .stroke(egui::Stroke::NONE) .rounding(egui::Rounding::same(crate::AVATAR_SIZE_F32)) - .fill(self.theme.accent_color()), + .fill(fill_color), ); if response.clicked() { self.show_post_area = true; From f3e3a55a7d501daf2bc5c641a69ef348ab2dcaf1 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Wed, 20 Mar 2024 16:39:53 -0600 Subject: [PATCH 13/29] Fixed #552: The "more actions" button in the list view should not be displayed if it is empty (Followed and Muted) --- gossip-bin/src/ui/people/list.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 5c2c76ff..75ce58f4 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -684,6 +684,11 @@ pub(super) fn render_more_list_actions( count: usize, on_list: bool, ) { + // do not show for "Following" and "Muted" when they are empty + if !on_list && !matches!(list, PersonList::Custom(_)) && count == 0 { + return; + } + if on_list { app.theme.accent_button_1_style(ui.style_mut()); } From 76c65929aa38ab3706073aea6215beb36a22daa0 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Thu, 21 Mar 2024 11:08:38 +1300 Subject: [PATCH 14/29] Fixes to relays being retried too often (penalty box for overlord, outside of picker) --- gossip-lib/src/comms.rs | 11 ++- gossip-lib/src/globals.rs | 9 +- gossip-lib/src/overlord/mod.rs | 150 ++++++++++++++++++++------------- 3 files changed, 109 insertions(+), 61 deletions(-) diff --git a/gossip-lib/src/comms.rs b/gossip-lib/src/comms.rs index 91030ab8..69335269 100644 --- a/gossip-lib/src/comms.rs +++ b/gossip-lib/src/comms.rs @@ -143,7 +143,7 @@ pub enum ToOverlordMessage { RankRelay(RelayUrl, u8), /// internal (the overlord sends messages to itself sometimes!) - ReengageMinion(RelayUrl, Vec), + ReengageMinion(RelayUrl), /// Calls [refresh_scores_and_pick_relays](crate::Overlord::refresh_scores_and_pick_relays) RefreshScoresAndPickRelays, @@ -235,7 +235,7 @@ pub(crate) struct ToMinionPayload { pub detail: ToMinionPayloadDetail, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub(crate) enum ToMinionPayloadDetail { AdvertiseRelayList(Box), AuthApproved, @@ -354,3 +354,10 @@ pub struct RelayJob { // overlord.minions_task_url // GLOBALS.relay_picker } + +impl RelayJob { + // This is like equality, but ignores the random job id + pub fn matches(&self, other: &RelayJob) -> bool { + self.reason == other.reason && self.payload.detail == other.payload.detail + } +} diff --git a/gossip-lib/src/globals.rs b/gossip-lib/src/globals.rs index e60f3e89..8786f583 100644 --- a/gossip-lib/src/globals.rs +++ b/gossip-lib/src/globals.rs @@ -57,9 +57,15 @@ pub struct Globals { /// All nostr people records currently loaded into memory, keyed by pubkey pub people: People, - /// The relays currently connected to + /// The relays currently connected to. It tracks the jobs that relay is assigned. + /// As the minion completes jobs, code modifies this jobset, removing those completed + /// jobs. pub connected_relays: DashMap>, + /// The relays not connected to, and which we will not connect to again until some + /// time passes, but which we still have jobs for + pub penalty_box_relays: DashMap>, + /// The relay picker, used to pick the next relay pub relay_picker: RelayPicker, @@ -186,6 +192,7 @@ lazy_static! { tmp_overlord_receiver: Mutex::new(Some(tmp_overlord_receiver)), people: People::new(), connected_relays: DashMap::new(), + penalty_box_relays: DashMap::new(), relay_picker: Default::default(), identity: GossipIdentity::default(), dismissed: RwLock::new(Vec::new()), diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index a89976e9..f216f1f5 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -363,6 +363,14 @@ impl Overlord { } refmut.value_mut().push(job); } + } else if GLOBALS.penalty_box_relays.contains_key(&url) { + // It is in the penalty box. + // To avoid a race condition with the task that removes it from the penalty + // box we have to use entry to make sure it was still there + GLOBALS + .penalty_box_relays + .entry(url) + .and_modify(|existing_jobs| Self::extend_jobs(existing_jobs, jobs)); } else { // Start up the minion let mut minion = Minion::new(url.clone()).await?; @@ -401,11 +409,15 @@ impl Overlord { // Remove from our hashmap self.minions_task_url.remove(&id); - // Set to not connected - let relayjobs = GLOBALS.connected_relays.remove(&url).map(|(_, v)| v); + // Set to not connected, and take any unfinished jobs + let mut relayjobs = match GLOBALS.connected_relays.remove(&url).map(|(_, v)| v) { + Some(jobs) => jobs, + None => vec![], + }; + // Exclusion will be non-zero if there was a failure. It will be zero if we + // succeeded let mut exclusion: u64; - let mut completed: bool = false; match join_result { Err(join_error) => { @@ -422,36 +434,37 @@ impl Overlord { } exclusion = match exitreason { MinionExitReason::GotDisconnected => 120, + MinionExitReason::GotShutdownMessage => 0, MinionExitReason::GotWSClose => 120, + MinionExitReason::LostOverlord => 0, + MinionExitReason::SubscriptionsHaveCompleted => { + relayjobs = vec![]; + 0 + } MinionExitReason::Unknown => 120, - MinionExitReason::SubscriptionsHaveCompleted => 5, - _ => 5, }; - - // Remember if the relay says all the jobs have completed - if matches!(exitreason, MinionExitReason::SubscriptionsHaveCompleted) { - completed = true; - } } Err(e) => { Self::bump_failure_count(&url); tracing::error!("Minion {} completed with error: {}", &url, e); exclusion = 120; if let ErrorKind::RelayRejectedUs = e.kind { - exclusion = 60 * 60 * 24 * 365; // don't connect again, practically + exclusion = u64::MAX; + } else if let ErrorKind::ReqwestHttpError(_) = e.kind { + exclusion = u64::MAX; } else if let ErrorKind::Websocket(wserror) = e.kind { if let tungstenite::error::Error::Http(response) = wserror { exclusion = match response.status() { - StatusCode::MOVED_PERMANENTLY => 60 * 60 * 24, - StatusCode::PERMANENT_REDIRECT => 60 * 60 * 24, - StatusCode::UNAUTHORIZED => 60 * 60 * 24, - StatusCode::PAYMENT_REQUIRED => 60 * 60 * 24, - StatusCode::FORBIDDEN => 60 * 60 * 24, - StatusCode::NOT_FOUND => 60 * 60 * 24, - StatusCode::PROXY_AUTHENTICATION_REQUIRED => 60 * 60 * 24, - StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => 60 * 60 * 24, - StatusCode::NOT_IMPLEMENTED => 60 * 60 * 24, - StatusCode::BAD_GATEWAY => 60 * 60 * 24, + StatusCode::MOVED_PERMANENTLY => u64::MAX, + StatusCode::PERMANENT_REDIRECT => u64::MAX, + StatusCode::UNAUTHORIZED => u64::MAX, + StatusCode::PAYMENT_REQUIRED => u64::MAX, + StatusCode::FORBIDDEN => u64::MAX, + StatusCode::NOT_FOUND => u64::MAX, + StatusCode::PROXY_AUTHENTICATION_REQUIRED => u64::MAX, + StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => u64::MAX, + StatusCode::NOT_IMPLEMENTED => u64::MAX, + StatusCode::BAD_GATEWAY => u64::MAX, s if s.as_u16() >= 400 => 120, _ => 120, }; @@ -472,9 +485,9 @@ impl Overlord { }, }; - // We might need to act upon this minion exiting + // Act upon this minion exiting, unless we are quitting if self.read_runstate.borrow().going_online() { - self.recover_from_minion_exit(url, relayjobs, exclusion, completed) + self.recover_from_minion_exit(url, relayjobs, exclusion) .await; } } @@ -482,9 +495,8 @@ impl Overlord { async fn recover_from_minion_exit( &mut self, url: RelayUrl, - jobs: Option>, + jobs: Vec, exclusion: u64, - completed: bool, ) { // Let the relay picker know it disconnected GLOBALS @@ -497,38 +509,52 @@ impl Overlord { } self.pick_relays().await; - if let Some(mut jobs) = jobs { - // Remove any advertise jobs from the active set - for job in &jobs { - GLOBALS.active_advertise_jobs.remove(&job.payload.job_id); - } - - if completed { - return; - } - - // If we have any persistent jobs, restart after a delay - let persistent_jobs: Vec = jobs - .drain(..) - .filter(|job| job.reason.persistent()) - .collect(); - - if !persistent_jobs.is_empty() { - // Do it after a delay - std::mem::drop(tokio::spawn(async move { - // Delay for exclusion first - tracing::info!( - "Minion {} will restart in {} seconds to continue persistent jobs", - &url, - exclusion - ); - tokio::time::sleep(Duration::new(exclusion, 0)).await; - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::ReengageMinion(url, persistent_jobs)); - })); - } + if exclusion == 0 { + return; } + + // Remove any advertise jobs from the active set + for job in &jobs { + GLOBALS.active_advertise_jobs.remove(&job.payload.job_id); + } + + if jobs.is_empty() { + return; + } + + // OK we have an exclusion and unfinished jobs. + // + // Add this relay to the penalty box, and setup a task to reengage + // it after the exclusion completes + let exclusion = exclusion.max(10); // safety catch, minimum exclusion is 10s + + GLOBALS.penalty_box_relays.insert(url.clone(), jobs); + + tracing::info!( + "Minion {} will restart in {} seconds to continue persistent jobs", + &url, + exclusion + ); + + if exclusion != u64::MAX { + // Re-engage after the delay + std::mem::drop(tokio::spawn(async move { + tokio::time::sleep(Duration::new(exclusion, 0)).await; + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::ReengageMinion(url)); + })); + } + // otherwise leave it in the penalty box forever + } + + async fn reengage_minion(&mut self, url: RelayUrl) -> Result<(), Error> { + // Take from penalty box + if let Some(pair) = GLOBALS.penalty_box_relays.remove(&url) { + self.engage_minion(url, pair.1).await?; + } + + Ok(()) } fn bump_failure_count(url: &RelayUrl) { @@ -538,6 +564,14 @@ impl Overlord { } } + fn extend_jobs(jobs: &mut Vec, mut more: Vec) { + for newjob in more.drain(..) { + if !jobs.iter().any(|job| job.matches(&newjob)) { + jobs.push(newjob) + } + } + } + async fn handle_message(&mut self, message: ToOverlordMessage) -> Result<(), Error> { match message { ToOverlordMessage::AddPubkeyRelay(pubkey, relayurl) => { @@ -684,8 +718,8 @@ impl Overlord { ToOverlordMessage::RankRelay(relay_url, rank) => { Self::rank_relay(relay_url, rank)?; } - ToOverlordMessage::ReengageMinion(url, persistent_jobs) => { - self.engage_minion(url, persistent_jobs).await?; + ToOverlordMessage::ReengageMinion(url) => { + self.reengage_minion(url).await?; } ToOverlordMessage::RefreshSubscribedMetadata => { self.refresh_subscribed_metadata().await?; From c35f49f990c87ed2be493eed40f94836b092ff48 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 20 Mar 2024 09:23:50 +1300 Subject: [PATCH 15/29] Storage.read_or_create_person() --- gossip-lib/src/storage/mod.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index 4f45bdb2..c63203e7 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -2022,6 +2022,23 @@ impl Storage { self.read_person2(pubkey) } + /// Read a person record, create if missing + #[inline] + pub fn read_or_create_person<'a>( + &'a self, + pubkey: &PublicKey, + rw_txn: Option<&mut RwTxn<'a>>, + ) -> Result { + match self.read_person(pubkey)? { + Some(p) => Ok(p), + None => { + let person = Person::new(pubkey.to_owned()); + self.write_person(&person, rw_txn)?; + Ok(person) + } + } + } + /// Write a new person record only if missing pub fn write_person_if_missing<'a>( &'a self, From d83b7e8728dcce228478df48c1d5903f4b45a890 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 17 Mar 2024 07:16:03 +1300 Subject: [PATCH 16/29] Person: relay_list_last_sought (replaces relay_list_last_received) --- gossip-lib/src/people.rs | 4 ++-- gossip-lib/src/storage/migrations/m7.rs | 2 +- gossip-lib/src/storage/mod.rs | 2 +- gossip-lib/src/storage/types/person2.rs | 7 ++++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index 19379a15..9f51e8f9 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -140,7 +140,7 @@ impl People { if let Ok(vec) = GLOBALS.storage.filter_people(|p| { p.is_subscribed_to() - && p.relay_list_last_received < stale + && p.relay_list_last_sought < stale && among_these.contains(&p.pubkey) }) { vec.iter().map(|p| p.pubkey).collect() @@ -826,7 +826,7 @@ impl People { None => Person::new(pubkey), }; - person.relay_list_last_received = now; + person.relay_list_last_sought = now; if let Some(old_at) = person.relay_list_created_at { if created_at < old_at { retval = false; diff --git a/gossip-lib/src/storage/migrations/m7.rs b/gossip-lib/src/storage/migrations/m7.rs index eb6f5012..7c0de534 100644 --- a/gossip-lib/src/storage/migrations/m7.rs +++ b/gossip-lib/src/storage/migrations/m7.rs @@ -32,7 +32,7 @@ impl Storage { nip05_valid: person1.nip05_valid, nip05_last_checked: person1.nip05_last_checked, relay_list_created_at: person1.relay_list_created_at, - relay_list_last_received: person1.relay_list_last_received, + relay_list_last_sought: person1.relay_list_last_received, }; self.write_person2(&person2, Some(txn))?; count += 1; diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index c63203e7..738f14df 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -1167,7 +1167,7 @@ impl Storage { if let Some(mut person) = self.read_person(&event.pubkey)? { // Mark that we received it. // This prevents us from refetching again and again. - person.relay_list_last_received = Unixtime::now().unwrap().0; + person.relay_list_last_sought = Unixtime::now().unwrap().0; // Check if this relay list is newer than the stamp we have for its author if let Some(previous_at) = person.relay_list_created_at { diff --git a/gossip-lib/src/storage/types/person2.rs b/gossip-lib/src/storage/types/person2.rs index 4192d2c2..e531f3a4 100644 --- a/gossip-lib/src/storage/types/person2.rs +++ b/gossip-lib/src/storage/types/person2.rs @@ -34,9 +34,10 @@ pub struct Person2 { /// for an update) pub relay_list_created_at: Option, - /// When their relay list was last received (to determine if we need to + /// When their relay list was last sought (to determine if we need to /// check for an update) - pub relay_list_last_received: i64, + #[serde(rename = "relay_list_last_received")] + pub relay_list_last_sought: i64, } impl Person2 { @@ -50,7 +51,7 @@ impl Person2 { nip05_valid: false, nip05_last_checked: None, relay_list_created_at: None, - relay_list_last_received: 0, + relay_list_last_sought: 0, } } From b53d9c53f87bab162cc22ebb9477c9a15328c5ab Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 20 Mar 2024 09:24:03 +1300 Subject: [PATCH 17/29] Change when person.relay_list_last_sought is updated (when we seek, not when we receive) --- gossip-lib/src/overlord/mod.rs | 14 ++++++++++++++ gossip-lib/src/people.rs | 3 --- gossip-lib/src/storage/mod.rs | 4 ---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index f216f1f5..3ee5e98c 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -2874,11 +2874,25 @@ impl Overlord { /// Subscribe to the multiple user's relay lists (optionally on the given relays, otherwise using /// theconfigured discover relays) + /// + /// Caller should probably check Person.relay_list_last_sought first to make sure we don't + /// already have an in-flight request doing this. pub async fn subscribe_discover( &mut self, pubkeys: Vec, relays: Option>, ) -> Result<(), Error> { + // Mark for each person that we are seeking their relay list + // so that we don't repeat this for a while + let now = Unixtime::now().unwrap(); + let mut txn = GLOBALS.storage.get_write_txn()?; + for pk in pubkeys.iter() { + let mut person = GLOBALS.storage.read_or_create_person(pk, Some(&mut txn))?; + person.relay_list_last_sought = now.0; + GLOBALS.storage.write_person(&person, Some(&mut txn))?; + } + txn.commit()?; + // Discover their relays let discover_relay_urls: Vec = match relays { Some(r) => r, diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index 9f51e8f9..a954f5f6 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -817,8 +817,6 @@ impl People { pubkey: PublicKey, created_at: i64, ) -> Result { - let now = Unixtime::now().unwrap().0; - let mut retval = false; let mut person = match GLOBALS.storage.read_person(&pubkey)? { @@ -826,7 +824,6 @@ impl People { None => Person::new(pubkey), }; - person.relay_list_last_sought = now; if let Some(old_at) = person.relay_list_created_at { if created_at < old_at { retval = false; diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index 738f14df..c6513ce0 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -1165,10 +1165,6 @@ impl Storage { let mut txn = self.env.write_txn()?; if let Some(mut person) = self.read_person(&event.pubkey)? { - // Mark that we received it. - // This prevents us from refetching again and again. - person.relay_list_last_sought = Unixtime::now().unwrap().0; - // Check if this relay list is newer than the stamp we have for its author if let Some(previous_at) = person.relay_list_created_at { if event.created_at.0 <= previous_at { From ecf72ae22b609ff59c7019d695e0df3ace6bf4a2 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 17 Mar 2024 07:16:03 +1300 Subject: [PATCH 18/29] People::person_needs_relay_list(pk) --- gossip-lib/src/misc.rs | 7 +++++++ gossip-lib/src/overlord/mod.rs | 4 +++- gossip-lib/src/people.rs | 24 ++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/gossip-lib/src/misc.rs b/gossip-lib/src/misc.rs index 97de765b..5d7a06b3 100644 --- a/gossip-lib/src/misc.rs +++ b/gossip-lib/src/misc.rs @@ -9,3 +9,10 @@ pub enum ZapState { LoadingInvoice(Id, PublicKey), ReadyToPay(Id, String), // String is the Zap Invoice as a string, to be shown as a QR code } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Freshness { + NeverSought, + Stale, + Fresh, +} diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 3ee5e98c..5dce313e 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -2876,7 +2876,9 @@ impl Overlord { /// theconfigured discover relays) /// /// Caller should probably check Person.relay_list_last_sought first to make sure we don't - /// already have an in-flight request doing this. + /// already have an in-flight request doing this. This can be done with: + /// GLOBALS.people.person_needs_relay_list() + /// GLOBALS.people.get_subscribed_pubkeys_needing_relay_lists() pub async fn subscribe_discover( &mut self, pubkeys: Vec, diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index a954f5f6..26fa95e0 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -1,6 +1,7 @@ use crate::comms::ToOverlordMessage; use crate::error::{Error, ErrorKind}; use crate::globals::GLOBALS; +use crate::misc::Freshness; use dashmap::{DashMap, DashSet}; use image::RgbaImage; use nostr_types::{ @@ -149,6 +150,29 @@ impl People { } } + /// Get if a person needs a relay list + pub fn person_needs_relay_list(pubkey: PublicKey) -> Freshness { + let staletime = Unixtime::now().unwrap().0 + - 60 * 60 + * GLOBALS + .storage + .read_setting_relay_list_becomes_stale_hours() as i64; + + match GLOBALS.storage.read_person(&pubkey) { + Err(_) => Freshness::NeverSought, + Ok(None) => Freshness::NeverSought, + Ok(Some(p)) => { + if p.relay_list_last_sought == 0 { + Freshness::NeverSought + } else if p.relay_list_last_sought < staletime { + Freshness::Stale + } else { + Freshness::Fresh + } + } + } + } + /// Create person record for this pubkey, if missing pub fn create_if_missing(&self, pubkey: PublicKey) { if let Err(e) = self.create_all_if_missing(&[pubkey]) { From dd5ed225567212f670d5fda3a1f659bbbe549b06 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 17 Mar 2024 08:42:44 +1300 Subject: [PATCH 19/29] Seeker (not yet in use) --- gossip-lib/src/globals.rs | 5 + gossip-lib/src/lib.rs | 6 + gossip-lib/src/seeker.rs | 234 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 245 insertions(+) create mode 100644 gossip-lib/src/seeker.rs diff --git a/gossip-lib/src/globals.rs b/gossip-lib/src/globals.rs index 8786f583..2e16b826 100644 --- a/gossip-lib/src/globals.rs +++ b/gossip-lib/src/globals.rs @@ -11,6 +11,7 @@ use crate::pending::Pending; use crate::people::{People, Person}; use crate::relay::Relay; use crate::relay_picker_hooks::Hooks; +use crate::seeker::Seeker; use crate::status::StatusQueue; use crate::storage::Storage; use crate::RunState; @@ -81,6 +82,9 @@ pub struct Globals { /// Fetcher pub fetcher: Fetcher, + /// Seeker + pub seeker: Seeker, + /// Failed Avatars /// If in this map, the avatar failed to load or process and is unrecoverable /// (but we will take them out and try again if new metadata flows in) @@ -198,6 +202,7 @@ lazy_static! { dismissed: RwLock::new(Vec::new()), feed: Feed::new(), fetcher: Fetcher::new(), + seeker: Seeker::new(), failed_avatars: RwLock::new(HashSet::new()), pixels_per_point_times_100: AtomicU32::new(139), // 100 dpi, 1/72th inch => 1.38888 status_queue: PRwLock::new(StatusQueue::new( diff --git a/gossip-lib/src/lib.rs b/gossip-lib/src/lib.rs index d24c3e29..51976613 100644 --- a/gossip-lib/src/lib.rs +++ b/gossip-lib/src/lib.rs @@ -136,6 +136,9 @@ pub use relay::Relay; mod relay_picker_hooks; pub use relay_picker_hooks::Hooks; +mod seeker; +pub use seeker::Seeker; + mod status; pub use status::StatusQueue; @@ -243,6 +246,9 @@ pub async fn run() { // Start the fetcher crate::fetcher::Fetcher::start(); + // Start the seeker + crate::seeker::Seeker::start(); + // Start periodic tasks in people manager (after signer) crate::people::People::start(); diff --git a/gossip-lib/src/seeker.rs b/gossip-lib/src/seeker.rs new file mode 100644 index 00000000..5b83e55d --- /dev/null +++ b/gossip-lib/src/seeker.rs @@ -0,0 +1,234 @@ +use crate::comms::ToOverlordMessage; +use crate::error::Error; +use crate::globals::GLOBALS; +use crate::misc::Freshness; +use crate::people::People; +use dashmap::DashMap; +use nostr_types::{Id, PublicKey, RelayUrl, RelayUsage, Unixtime}; +use std::time::Duration; +use tokio::time::Instant; + +#[derive(Debug, Clone)] +pub enum SeekState { + WaitingRelayList(Unixtime, PublicKey), + WaitingEvent(Unixtime), +} + +#[derive(Debug, Default)] +pub struct Seeker { + events: DashMap, +} + +impl Seeker { + pub(crate) fn new() -> Seeker { + Seeker { + ..Default::default() + } + } + + fn get_relays(author: PublicKey) -> Result, Error> { + Ok(GLOBALS + .storage + .get_best_relays(author, RelayUsage::Outbox)? + .iter() + .map(|(r, _)| r.to_owned()) + .collect()) + } + + fn seek_relay_list(author: PublicKey) { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::SubscribeDiscover(vec![author], None)); + } + + fn seek_event_at_our_read_relays(id: Id) { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::FetchEvent(id, vec![])); + } + + fn seek_event_at_relays(id: Id, relays: Vec) { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::FetchEvent(id, relays)); + } + + /// Seek an event when you only have the `Id` + pub(crate) fn seek_id(&self, id: Id) { + if self.events.get(&id).is_some() { + return; // we are already seeking this event + } + + tracing::debug!("Seeking id={}", id.as_hex_string()); + + Self::seek_event_at_our_read_relays(id); + + // Remember when we asked + let now = Unixtime::now().unwrap(); + self.events.insert(id, SeekState::WaitingEvent(now)); + } + + /// Seek an event when you have the `Id` and the author `PublicKey` + pub(crate) fn seek_id_and_author(&self, id: Id, author: PublicKey) -> Result<(), Error> { + if self.events.get(&id).is_some() { + return Ok(()); // we are already seeking this event + } + + tracing::debug!( + "Seeking id={} with author={}", + id.as_hex_string(), + author.as_hex_string() + ); + + let when = Unixtime::now().unwrap(); + + // Check if we have the author's relay list + match People::person_needs_relay_list(author) { + Freshness::NeverSought => { + Self::seek_relay_list(author); + self.events + .insert(id, SeekState::WaitingRelayList(when, author)); + } + Freshness::Stale => { + // Seek the relay list because it is stale, but don't let that hold us up + // using the stale data + Self::seek_relay_list(author); + + let relays = Self::get_relays(author)?; + Self::seek_event_at_relays(id, relays); + self.events.insert(id, SeekState::WaitingEvent(when)); + } + Freshness::Fresh => { + let relays = Self::get_relays(author)?; + Self::seek_event_at_relays(id, relays); + self.events.insert(id, SeekState::WaitingEvent(when)); + } + } + + Ok(()) + } + + /// Seek an event when you have the `Id` and the relays to seek from + pub(crate) fn seek_id_and_relays(&self, id: Id, relays: Vec) { + if let Some(existing) = self.events.get(&id) { + if matches!(existing.value(), SeekState::WaitingEvent(_)) { + return; // Already seeking it + } + } + let when = Unixtime::now().unwrap(); + Self::seek_event_at_relays(id, relays); + self.events.insert(id, SeekState::WaitingEvent(when)); + } + + /// Inform the seeker that an author's relay list has just arrived + pub(crate) fn found_author_relays(&self, pubkey: PublicKey) { + // Instead of updating the map while we iterate (which could deadlock) + // we save updates here and apply when the iterator is finished. + let mut updates: Vec<(Id, SeekState)> = Vec::new(); + + for refmutmulti in self.events.iter_mut() { + if let SeekState::WaitingRelayList(_when, author) = refmutmulti.value() { + if *author == pubkey { + let now = Unixtime::now().unwrap(); + let id = *refmutmulti.key(); + if let Ok(relays) = Self::get_relays(*author) { + Self::seek_event_at_relays(id, relays); + updates.push((id, SeekState::WaitingEvent(now))); + } + } + } + } + + for (id, state) in updates.drain(..) { + let _ = self.events.insert(id, state); + } + } + + pub(crate) fn found_or_cancel(&self, id: Id) { + self.events.remove(&id); + } + + pub(crate) fn start() { + // Setup periodic queue management + tokio::task::spawn(async move { + let mut read_runstate = GLOBALS.read_runstate.clone(); + read_runstate.mark_unchanged(); + if read_runstate.borrow().going_offline() { + return; + } + + let sleep = tokio::time::sleep(Duration::from_millis(1000)); + tokio::pin!(sleep); + + loop { + tokio::select! { + _ = &mut sleep => { + sleep.as_mut().reset(Instant::now() + Duration::from_millis(1000)); + }, + _ = read_runstate.wait_for(|runstate| runstate.going_offline()) => break, + } + + GLOBALS.seeker.run_once().await; + } + + tracing::info!("Seeker shutdown"); + }); + } + + pub(crate) async fn run_once(&self) { + if self.events.is_empty() { + return; + } + + // Instead of updating the map while we iterate (which could deadlock) + // we save updates here and apply when the iterator is finished. + let mut updates: Vec<(Id, Option)> = Vec::new(); + + let now = Unixtime::now().unwrap(); + + for refmulti in self.events.iter() { + let id = *refmulti.key(); + match refmulti.value() { + SeekState::WaitingRelayList(when, author) => { + // Check if we have their relays yet + match People::person_needs_relay_list(*author) { + Freshness::Fresh | Freshness::Stale => { + if let Ok(relays) = Self::get_relays(*author) { + Self::seek_event_at_relays(id, relays); + updates.push((id, Some(SeekState::WaitingEvent(now)))); + continue; + } + } + _ => {} + } + + // If it has been 15 seconds, give up the wait and seek from our READ relays + if now - *when > Duration::from_secs(15) { + Self::seek_event_at_our_read_relays(id); + updates.push((id, Some(SeekState::WaitingEvent(now)))); + } + + // Otherwise keep waiting + } + SeekState::WaitingEvent(when) => { + if now - *when > Duration::from_secs(15) { + tracing::debug!("Failed to find id={}", id.as_hex_string()); + updates.push((id, None)); + } + } + } + } + + // Apply updates + for (id, replacement) in updates.drain(..) { + match replacement { + Some(state) => { + let _ = self.events.insert(id, state); + } + None => { + let _ = self.events.remove(&id); + } + } + } + } +} From 9cd8c6bdd3da7062b0cf2826cfc7f72036093422 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 20 Mar 2024 11:08:55 +1300 Subject: [PATCH 20/29] Integrate seeker into process --- gossip-lib/src/process.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gossip-lib/src/process.rs b/gossip-lib/src/process.rs index ff248d47..e8156e7d 100644 --- a/gossip-lib/src/process.rs +++ b/gossip-lib/src/process.rs @@ -239,6 +239,9 @@ pub async fn process_new_event( // Invalidate UI events indicated by those relationships GLOBALS.ui_notes_to_invalidate.write().extend(&invalid_ids); + // Let seeker know about this event id (in case it was sought) + GLOBALS.seeker.found_or_cancel(event.id); + // If metadata, update person if event.kind == EventKind::Metadata { let metadata: Metadata = serde_json::from_str(&event.content)?; @@ -273,6 +276,11 @@ pub async fn process_new_event( } else if event.kind == EventKind::RelayList { GLOBALS.storage.process_relay_list(event)?; + // Let the seeker know we now have relays for this author, in case the seeker + // wants to update it's state + // (we might not, but by this point we have tried) + GLOBALS.seeker.found_author_relays(event.pubkey); + // the following also refreshes scores before it picks relays let _ = GLOBALS .to_overlord From ad364a5b365f6942c590f276d8d95058067e204e Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 20 Mar 2024 11:04:56 +1300 Subject: [PATCH 21/29] Reposts: if inner json event: seek relay list of inner author, then seek inner event --- gossip-lib/src/process.rs | 40 +++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/gossip-lib/src/process.rs b/gossip-lib/src/process.rs index e8156e7d..92f35609 100644 --- a/gossip-lib/src/process.rs +++ b/gossip-lib/src/process.rs @@ -286,21 +286,33 @@ pub async fn process_new_event( .to_overlord .send(ToOverlordMessage::RefreshScoresAndPickRelays); } else if event.kind == EventKind::Repost { - // If the content is a repost, seek the event it reposts - for eref in event.mentions().iter() { - match eref { - EventReference::Id(id, optrelay, _marker) => { - if let Some(rurl) = optrelay { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::FetchEvent(*id, vec![rurl.to_owned()])); + // If it has a json encoded inner event + if let Ok(inner_event) = serde_json::from_str::(&event.content) { + // Seek that event by id and author + GLOBALS + .seeker + .seek_id_and_author(inner_event.id, inner_event.pubkey)?; + } else { + // If the content is a repost, seek the event it reposts + for eref in event.mentions().iter() { + match eref { + EventReference::Id(id, optrelay, _marker) => { + if let Some(rurl) = optrelay { + GLOBALS + .seeker + .seek_id_and_relays(*id, vec![rurl.to_owned()]); + } else { + // Even if the event tags the author, we have no way to coorelate + // the nevent with that tag. + GLOBALS.seeker.seek_id(*id); + } } - } - EventReference::Addr(ea) => { - if !ea.relays.is_empty() { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::FetchEventAddr(ea.clone())); + EventReference::Addr(ea) => { + if !ea.relays.is_empty() { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::FetchEventAddr(ea.clone())); + } } } } From 4389b1cfdaa6ae6bd60b16b47a6ec2ef59ec70d7 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 20 Mar 2024 10:18:37 +1300 Subject: [PATCH 22/29] Seek relay list when following someone (parameter not necessary), but only if needed --- gossip-bin/src/ui/mod.rs | 21 +++++++-------------- gossip-lib/src/nip05.rs | 2 +- gossip-lib/src/overlord/mod.rs | 4 ++-- gossip-lib/src/people.rs | 19 ++++++++++++------- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index e19b4b8d..9238089d 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -1592,21 +1592,14 @@ impl GossipUi { } } if !followed && ui.button("Follow").clicked() { - let _ = GLOBALS.people.follow( - &person.pubkey, - true, - PersonList::Followed, - true, - true, - ); + let _ = GLOBALS + .people + .follow(&person.pubkey, true, PersonList::Followed, true); } else if followed && ui.button("Unfollow").clicked() { - let _ = GLOBALS.people.follow( - &person.pubkey, - false, - PersonList::Followed, - true, - false, - ); + let _ = + GLOBALS + .people + .follow(&person.pubkey, false, PersonList::Followed, true); } // Do not show 'Mute' if this is yourself diff --git a/gossip-lib/src/nip05.rs b/gossip-lib/src/nip05.rs index c4969165..2af860c7 100644 --- a/gossip-lib/src/nip05.rs +++ b/gossip-lib/src/nip05.rs @@ -122,7 +122,7 @@ pub async fn get_and_follow_nip05( update_relays(&nip05, nip05file, &pubkey).await?; // Follow - GLOBALS.people.follow(&pubkey, true, list, public, true)?; + GLOBALS.people.follow(&pubkey, true, list, public)?; tracing::info!("Followed {}", &nip05); diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 5dce313e..7d41e477 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -1420,7 +1420,7 @@ impl Overlord { list: PersonList, public: bool, ) -> Result<(), Error> { - GLOBALS.people.follow(&pubkey, true, list, public, true)?; + GLOBALS.people.follow(&pubkey, true, list, public)?; tracing::debug!("Followed {}", &pubkey.as_hex_string()); Ok(()) } @@ -1464,7 +1464,7 @@ impl Overlord { // Follow GLOBALS .people - .follow(&nprofile.pubkey, true, list, public, true)?; + .follow(&nprofile.pubkey, true, list, public)?; GLOBALS .status_queue diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index 26fa95e0..b68ef0e9 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -753,7 +753,6 @@ impl People { follow: bool, list: PersonList, public: bool, - discover: bool, // if you also want to subscribe to their relay list ) -> Result<(), Error> { if follow { GLOBALS @@ -762,6 +761,18 @@ impl People { // Add to the relay picker. If they are already there, it will be ok. GLOBALS.relay_picker.add_someone(*pubkey)?; + + // Maybe seek relay list (if needed) + let seek_relay_list = match Self::person_needs_relay_list(*pubkey) { + Freshness::NeverSought => true, + Freshness::Stale => true, + Freshness::Fresh => false, + }; + if seek_relay_list { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::SubscribeDiscover(vec![*pubkey], None)); + }; } else { GLOBALS .storage @@ -777,12 +788,6 @@ impl People { .to_overlord .send(ToOverlordMessage::RefreshScoresAndPickRelays); - if follow && discover { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::SubscribeDiscover(vec![*pubkey], None)); - } - Ok(()) } From 30cf111a7a849385aec2b021dca46aa05433ed67 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 20 Mar 2024 10:57:57 +1300 Subject: [PATCH 23/29] Minion no longer subscribing to relay lists in general feed --- gossip-lib/src/overlord/minion/filter_fns.rs | 26 -------------------- gossip-lib/src/overlord/minion/mod.rs | 8 ++---- 2 files changed, 2 insertions(+), 32 deletions(-) diff --git a/gossip-lib/src/overlord/minion/filter_fns.rs b/gossip-lib/src/overlord/minion/filter_fns.rs index aff19d16..979fdd02 100644 --- a/gossip-lib/src/overlord/minion/filter_fns.rs +++ b/gossip-lib/src/overlord/minion/filter_fns.rs @@ -29,32 +29,6 @@ pub fn general_feed( filters } -pub fn relay_lists(authors: &[PublicKey]) -> Vec { - // Try to find where people post. - // Subscribe to kind-10002 `RelayList`s to see where people post. - // Subscribe to ContactLists so we can look at the contents and - // divine relays people write to (if using a client that does that). - // BUT ONLY for people where this kind of data hasn't been received - // in the last 8 hours (so we don't do it every client restart). - let keys_needing_relay_lists: Vec = GLOBALS - .people - .get_subscribed_pubkeys_needing_relay_lists(authors) - .drain(..) - .map(|pk| pk.into()) - .collect(); - - if !keys_needing_relay_lists.is_empty() { - vec![Filter { - authors: keys_needing_relay_lists, - kinds: vec![EventKind::RelayList, EventKind::ContactList], - // No since. These are replaceable events, we should only get 1 per person. - ..Default::default() - }] - } else { - vec![] - } -} - pub fn augments(ids: &[IdHex]) -> Vec { let event_kinds = crate::feed::feed_augment_event_kinds(); diff --git a/gossip-lib/src/overlord/minion/mod.rs b/gossip-lib/src/overlord/minion/mod.rs index d477d6f5..e3544cd4 100644 --- a/gossip-lib/src/overlord/minion/mod.rs +++ b/gossip-lib/src/overlord/minion/mod.rs @@ -638,9 +638,7 @@ impl Minion { } }; - let mut filters = filter_fns::general_feed(&self.general_feed_keys, since, None); - let filters2 = filter_fns::relay_lists(&self.general_feed_keys); - filters.extend(filters2); + let filters = filter_fns::general_feed(&self.general_feed_keys, since, None); if filters.is_empty() { self.unsubscribe("general_feed").await?; @@ -682,9 +680,7 @@ impl Minion { None => self.compute_since(GLOBALS.storage.read_setting_feed_chunk()), }; - let mut filters = filter_fns::general_feed(&new_keys, since, None); - let filters2 = filter_fns::relay_lists(&new_keys); - filters.extend(filters2); + let filters = filter_fns::general_feed(&new_keys, since, None); if !filters.is_empty() { self.subscribe(filters, "temp_general_feed_update", job_id) From 560045143657859e74e4d1d12a69b8185e76ca92 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 20 Mar 2024 10:25:19 +1300 Subject: [PATCH 24/29] Overlord to subscribe to relay lists of everyone followed (only needed) --- gossip-lib/src/overlord/mod.rs | 4 ++-- gossip-lib/src/people.rs | 14 +++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 7d41e477..aaed3048 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -2815,8 +2815,8 @@ impl Overlord { } // Separately subscribe to RelayList discovery for everyone we follow - // We just do this once at startup. Relay lists don't change that frequently. - let followed = GLOBALS.people.get_subscribed_pubkeys(); + // who needs to seek a relay list again. + let followed = GLOBALS.people.get_subscribed_pubkeys_needing_relay_lists(); self.subscribe_discover(followed, None).await?; // Separately subscribe to our outbox events on our write relays diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index b68ef0e9..b1ca622f 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -129,21 +129,17 @@ impl People { } /// Get all the pubkeys that need relay lists (from the given set) - pub fn get_subscribed_pubkeys_needing_relay_lists( - &self, - among_these: &[PublicKey], - ) -> Vec { + pub fn get_subscribed_pubkeys_needing_relay_lists(&self) -> Vec { let stale = Unixtime::now().unwrap().0 - 60 * 60 * GLOBALS .storage .read_setting_relay_list_becomes_stale_hours() as i64; - if let Ok(vec) = GLOBALS.storage.filter_people(|p| { - p.is_subscribed_to() - && p.relay_list_last_sought < stale - && among_these.contains(&p.pubkey) - }) { + if let Ok(vec) = GLOBALS + .storage + .filter_people(|p| p.is_subscribed_to() && p.relay_list_last_sought < stale) + { vec.iter().map(|p| p.pubkey).collect() } else { vec![] From 8ddc6b99fb4f6c41cd23d731c49ed3d3e27606d7 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Wed, 20 Mar 2024 17:30:59 -0600 Subject: [PATCH 25/29] Fixed #552: The error messages in the "Add contact to the list" should be displayed below the title --- gossip-bin/src/ui/people/list.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 75ce58f4..050ab934 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -27,6 +27,7 @@ pub(in crate::ui) struct ListUi { add_contact_searched: Option, add_contact_search_results: Vec<(String, PublicKey)>, add_contact_search_selected: Option, + add_contact_error: Option, entering_follow_someone_on_list: bool, clear_list_needs_confirm: bool, @@ -49,6 +50,7 @@ impl ListUi { add_contact_searched: None, add_contact_search_results: Vec::new(), add_contact_search_selected: None, + add_contact_error: None, entering_follow_someone_on_list: false, clear_list_needs_confirm: false, @@ -350,7 +352,7 @@ fn render_add_contact_popup( list: PersonList, metadata: &PersonListMetadata, ) { - const DLG_SIZE: Vec2 = vec2(400.0, 240.0); + const DLG_SIZE: Vec2 = vec2(400.0, 260.0); let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, DLG_SIZE, true, |ui| { let enter_key; (app.people_list.add_contact_search_selected, enter_key) = @@ -367,6 +369,18 @@ fn render_add_contact_popup( ui.heading("Add contact to the list"); ui.add_space(8.0); + // error block + ui.label( + RichText::new( + app.people_list + .add_contact_error + .as_ref() + .unwrap_or(&"".to_string()), + ) + .color(app.theme.warning_marker_text_color()), + ); + ui.add_space(8.0); + ui.label("Search for known contacts to add"); ui.add_space(8.0); @@ -466,10 +480,7 @@ fn render_add_contact_popup( mark_refresh(app); } else { add_failed = true; - GLOBALS - .status_queue - .write() - .write("Invalid pubkey.".to_string()); + app.people_list.add_contact_error = Some("Invalid pubkey.".to_string()); } if !add_failed { app.add_contact = "".to_owned(); From c1a50ea44b4d7cd547caab711e86d83e37249647 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Wed, 20 Mar 2024 17:43:04 -0600 Subject: [PATCH 26/29] Fixes #552: Clicking on labels should also do row_click action --- gossip-bin/src/ui/people/list.rs | 37 ++++++++++++++-------- gossip-bin/src/ui/people/lists.rs | 51 +++++++++++++++++++++---------- 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 050ab934..cdd54d69 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -3,7 +3,7 @@ use std::time::{Duration, Instant}; use super::{GossipUi, Page}; use crate::ui::widgets; use crate::AVATAR_SIZE_F32; -use eframe::egui; +use eframe::egui::{self, Label, Sense}; use egui::{Context, RichText, Ui, Vec2}; use egui_winit::egui::text::LayoutJob; use egui_winit::egui::text_edit::TextEditOutput; @@ -220,7 +220,7 @@ pub(super) fn update( app.placeholder_avatar.clone() }; - let avatar_response = + let mut response = widgets::paint_avatar(ui, person, &avatar, widgets::AvatarSize::Feed); ui.add_space(20.0); @@ -228,7 +228,10 @@ pub(super) fn update( ui.vertical(|ui| { ui.add_space(5.0); ui.horizontal(|ui| { - ui.label(RichText::new(person.best_name()).size(15.5)); + response |= ui.add( + Label::new(RichText::new(person.best_name()).size(15.5)) + .sense(Sense::click()), + ); ui.add_space(10.0); @@ -237,14 +240,20 @@ pub(super) fn update( .have_persons_relays(person.pubkey) .unwrap_or(false) { - ui.label( - RichText::new("Relay list not found") - .color(app.theme.warning_marker_text_color()), + response |= ui.add( + Label::new( + RichText::new("Relay list not found") + .color(app.theme.warning_marker_text_color()), + ) + .sense(Sense::click()), ); } }); ui.add_space(3.0); - ui.label(GossipUi::richtext_from_person_nip05(person).weak()); + response |= ui.add( + Label::new(GossipUi::richtext_from_person_nip05(person).weak()) + .sense(Sense::click()), + ); }); ui.vertical(|ui| { @@ -284,17 +293,21 @@ pub(super) fn update( } }, ); - }); - if avatar_response.clicked() { - app.set_page(ctx, Page::Person(person.pubkey)); - } - }); + response + }) + .inner + }) + .inner }, ); if row_response .response .on_hover_cursor(egui::CursorIcon::PointingHand) .clicked() + || row_response + .inner + .on_hover_cursor(egui::CursorIcon::PointingHand) + .clicked() { app.set_page(ctx, Page::Person(person.pubkey)); } diff --git a/gossip-bin/src/ui/people/lists.rs b/gossip-bin/src/ui/people/lists.rs index 1429e8df..13bdc7ae 100644 --- a/gossip-bin/src/ui/people/lists.rs +++ b/gossip-bin/src/ui/people/lists.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use super::{GossipUi, Page}; use crate::ui::widgets; -use eframe::egui; +use eframe::egui::{self, Sense}; use egui::{Context, Ui}; use egui_winit::egui::{Label, RichText}; use gossip_lib::{PersonList, PersonListMetadata, GLOBALS}; @@ -55,22 +55,34 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.vertical(|ui| { ui.horizontal(|ui| { - ui.add(Label::new( - RichText::new(&metadata.title).heading().color(color), - )); - ui.label(format!("({})", metadata.len)); + let mut response = ui.add( + Label::new( + RichText::new(&metadata.title).heading().color(color), + ) + .sense(egui::Sense::click()), + ); + + response |= ui.add( + Label::new(format!("({})", metadata.len)).sense(Sense::click()), + ); if metadata.favorite { - ui.add(Label::new( - RichText::new("★") - .size(18.0) - .color(app.theme.accent_complementary_color()), - )); + response |= ui.add( + Label::new( + RichText::new("★") + .size(18.0) + .color(app.theme.accent_complementary_color()), + ) + .sense(Sense::click()), + ); } if metadata.private { - ui.add(Label::new( - RichText::new("😎") - .color(app.theme.accent_complementary_color()), - )); + response |= ui.add( + Label::new( + RichText::new("😎") + .color(app.theme.accent_complementary_color()), + ) + .sense(Sense::click()), + ); } ui.with_layout( @@ -87,14 +99,21 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ); }, ); - }); - }); + response + }) + .inner + }) + .inner }, ); if row_response .response .on_hover_cursor(egui::CursorIcon::PointingHand) .clicked() + || row_response + .inner + .on_hover_cursor(egui::CursorIcon::PointingHand) + .clicked() { app.set_page(ctx, Page::PeopleList(list)); } From fe9ccee35f4b2728b370f39a3f0c1e3153ff0e88 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Thu, 21 Mar 2024 15:37:09 +1300 Subject: [PATCH 27/29] get_subscribed_pubkeys() - always include the current user (in case they don't follow themselves) --- gossip-lib/src/people.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index 19379a15..d046d6ea 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -106,11 +106,19 @@ impl People { } /// Get all the pubkeys that the user subscribes to in any list + /// (We also force the current user into this list) pub fn get_subscribed_pubkeys(&self) -> Vec { // We subscribe to all people in all lists. // This is no longer synonomous with the ContactList list match GLOBALS.storage.get_people_in_all_followed_lists() { - Ok(people) => people, + Ok(mut people) => { + if let Some(pk) = GLOBALS.identity.public_key() { + if !people.contains(&pk) { + people.push(pk); + } + } + people + } Err(e) => { tracing::error!("{}", e); vec![] From eea6eccc8bb4044d015ca234e4f8e72989223676 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 21 Mar 2024 09:35:06 -0600 Subject: [PATCH 28/29] People Lists: Row entry text should be non-selectable --- gossip-bin/src/ui/people/list.rs | 5 ++++- gossip-bin/src/ui/people/lists.rs | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index cdd54d69..b214e6a2 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -230,6 +230,7 @@ pub(super) fn update( ui.horizontal(|ui| { response |= ui.add( Label::new(RichText::new(person.best_name()).size(15.5)) + .selectable(false) .sense(Sense::click()), ); @@ -245,6 +246,7 @@ pub(super) fn update( RichText::new("Relay list not found") .color(app.theme.warning_marker_text_color()), ) + .selectable(false) .sense(Sense::click()), ); } @@ -252,6 +254,7 @@ pub(super) fn update( ui.add_space(3.0); response |= ui.add( Label::new(GossipUi::richtext_from_person_nip05(person).weak()) + .selectable(false) .sense(Sense::click()), ); }); @@ -277,7 +280,7 @@ pub(super) fn update( if list != PersonList::Followed { // private / public switch - ui.label("Private"); + ui.add(Label::new("Private").selectable(false)); if ui .add(widgets::Switch::onoff(&app.theme, &mut private)) .clicked() diff --git a/gossip-bin/src/ui/people/lists.rs b/gossip-bin/src/ui/people/lists.rs index 13bdc7ae..43005836 100644 --- a/gossip-bin/src/ui/people/lists.rs +++ b/gossip-bin/src/ui/people/lists.rs @@ -59,11 +59,14 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra Label::new( RichText::new(&metadata.title).heading().color(color), ) + .selectable(false) .sense(egui::Sense::click()), ); response |= ui.add( - Label::new(format!("({})", metadata.len)).sense(Sense::click()), + Label::new(format!("({})", metadata.len)) + .selectable(false) + .sense(Sense::click()), ); if metadata.favorite { response |= ui.add( @@ -72,6 +75,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra .size(18.0) .color(app.theme.accent_complementary_color()), ) + .selectable(false) .sense(Sense::click()), ); } @@ -81,6 +85,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra RichText::new("😎") .color(app.theme.accent_complementary_color()), ) + .selectable(false) .sense(Sense::click()), ); } From d3e5dacd67c13a05c93dd143aaf8798e71cf39df Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 21 Mar 2024 10:31:49 -0600 Subject: [PATCH 29/29] Fix #552: Don't show more menu on "Followed" and "Muted", remove "view contacts" menu item from more menu for all --- gossip-bin/src/ui/people/list.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index b214e6a2..52b74357 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -711,8 +711,8 @@ pub(super) fn render_more_list_actions( count: usize, on_list: bool, ) { - // do not show for "Following" and "Muted" when they are empty - if !on_list && !matches!(list, PersonList::Custom(_)) && count == 0 { + // do not show for "Following" and "Muted" + if !on_list && !matches!(list, PersonList::Custom(_)) { return; } @@ -729,12 +729,6 @@ pub(super) fn render_more_list_actions( app.theme.accent_button_1_style(ui.style_mut()); ui.spacing_mut().item_spacing.y = 15.0; } - if !on_list { - if ui.button("View Contacts").clicked() { - app.set_page(ui.ctx(), Page::PeopleList(list)); - *is_open = false; - } - } if matches!(list, PersonList::Custom(_)) { if ui.button("Rename").clicked() { app.deleting_list = None;