diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 22e25b6d..5417d6cb 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -681,7 +681,6 @@ impl eframe::App for GossipUi { let (mut submenu, header_response) = self.get_openable_menu(ui, SubMenu::Relays, "Relays"); submenu.show_body_indented(&header_response, ui, |ui| { self.add_menu_item_page(ui, Page::RelaysActivityMonitor, "Active Relays"); - self.add_menu_item_page(ui, Page::RelaysCoverage, "Coverage"); self.add_menu_item_page(ui, Page::RelaysMine, "My Relays"); self.add_menu_item_page(ui, Page::RelaysKnownNetwork, "Known Network"); ui.vertical(|ui| { diff --git a/src/ui/relays/coverage.rs b/src/ui/relays/coverage.rs index 324a9d84..682fd9cc 100644 --- a/src/ui/relays/coverage.rs +++ b/src/ui/relays/coverage.rs @@ -1,65 +1,127 @@ -use egui_winit::egui::{Context, Ui, self, vec2, Response}; -use nostr_types::PublicKey; +use egui_winit::egui::{Context, Ui, self, vec2, Response, RichText, Align, Id}; +use nostr_types::{PublicKey, RelayUrl}; -use crate::{globals::GLOBALS, ui::{GossipUi, widgets, Page, SettingsTab}, comms::ToOverlordMessage}; +use crate::{globals::GLOBALS, ui::{GossipUi, widgets::{self, list_entry::{TEXT_TOP, TEXT_LEFT, self, draw_text_at, TEXT_RIGHT, allocate_text_at, draw_text_galley_at}}, Page, SettingsTab}, comms::ToOverlordMessage}; struct CoverageEntry<'a> { pk: &'a PublicKey, - count: &'a usize, + _count: &'a usize, + relays: Vec, name: String, } impl<'a> CoverageEntry<'a> { - pub(super) fn new(pk: &'a PublicKey, count: &'a usize) -> Self { - let name = GossipUi::display_name_from_pubkey_lookup(pk); + pub(super) fn new(pk: &'a PublicKey, name: String, _count: &'a usize, relays: Vec) -> Self { Self { pk, - count, + _count, + relays, name } } - pub(super) fn show(&self, ui: &mut Ui) -> Response { - let (rect, _) = widgets::list_entry::allocate_space(ui, 45.0); + fn make_id(&self, str: &str) -> Id { + (self.pk.as_hex_string() + str).into() + } - widgets::list_entry::paint_frame(ui, &rect); + pub(super) fn show(&self, ui: &mut Ui, app: &mut GossipUi) -> Response { + let available_width = ui.available_size_before_wrap().x; + let (rect, response) = ui.allocate_exact_size(vec2(available_width, 80.0), egui::Sense::click()); - let id = ui.auto_id_with(self.pk.as_hex_string()); - let pos = rect.min + vec2(widgets::list_entry::TEXT_LEFT, widgets::list_entry::TEXT_TOP); - let (galley, response) = widgets::list_entry::allocate_text_at( + let color = if response.hovered() { + Some(ui.style().visuals.extreme_bg_color.linear_multiply(0.2)) + } else { + None + }; + + widgets::list_entry::paint_frame(ui, &rect, color); + + // ---- title ---- + let pos = rect.min + vec2(TEXT_LEFT, TEXT_TOP); + draw_text_at( ui, pos, - self.name.clone().into(), - egui::Align::LEFT, - id); + RichText::new(self.name.clone()).size(list_entry::TITLE_FONT_SIZE).into(), + Align::LEFT, + Some(app.settings.theme.accent_color()), + None); - widgets::list_entry::draw_text_galley_at( + // ---- pubkey ---- + // copy button + { + let pos = rect.right_top() + vec2(-TEXT_RIGHT, TEXT_TOP); + let text = RichText::new(crate::ui::widgets::COPY_SYMBOL); + let id = self.make_id("copy-pubkey"); + let (galley, response) = allocate_text_at(ui, pos, text.into(), Align::RIGHT, id); + if response + .on_hover_cursor(egui::CursorIcon::PointingHand) + .clicked() { + ui.output_mut(|o| { + o.copied_text = self.pk.as_bech32_string(); + GLOBALS + .status_queue + .write() + .write("copied to clipboard".to_owned()); + }); + } + draw_text_galley_at(ui, pos, galley, None, None); + } + + // pubkey + let pos = rect.right_top() + vec2(-TEXT_RIGHT - 20.0, TEXT_TOP); + draw_text_at( ui, pos, - galley, - None, - None); - - widgets::list_entry::draw_text_at( - ui, - pos + vec2(response.rect.width(), 0.0), - format!(": coverage short by {} relay(s)", self.count).into(), - egui::Align::LEFT, + self.pk.as_bech32_string().into(), + Align::RIGHT, None, None); - let response = response - .on_hover_text(format!("Go to profile of {}", self.name)) - .on_hover_cursor(egui::CursorIcon::PointingHand); + // ---- connected relays ---- + let pos = rect.min + vec2(TEXT_LEFT, TEXT_TOP + 30.0); + let relays_string = self.relays.iter().map(|f| f.to_string()).collect::>().join(", "); + draw_text_at( + ui, + pos, + relays_string.into(), + Align::LEFT, + None, + None); response } } +fn find_relays_for_pubkey(pk: &PublicKey) -> Vec { + GLOBALS.relay_picker.relay_assignments_iter() + .filter(|f| f.pubkeys.contains(pk)) + .map(|f| f.relay_url.clone()) + .collect() +} + pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { ui.add_space(10.0); ui.horizontal_wrapped(|ui| { - ui.heading("Coverage Report"); + ui.heading(format!("Low Coverage Report (less than {} relays)", app.settings.num_relays_per_person)); + ui.add_space(10.0); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| { + ui.add_space(20.0); + ui.spacing_mut().button_padding *= 2.0; + { + let visuals = ui.visuals_mut(); + visuals.widgets.inactive.weak_bg_fill = app.settings.theme.accent_color(); + visuals.widgets.inactive.fg_stroke.width = 1.0; + visuals.widgets.inactive.fg_stroke.color = app.settings.theme.get_style().visuals.extreme_bg_color; + visuals.widgets.hovered.weak_bg_fill = app.settings.theme.navigation_text_color(); + visuals.widgets.hovered.fg_stroke.color = app.settings.theme.accent_color(); + visuals.widgets.inactive.fg_stroke.color = app.settings.theme.get_style().visuals.extreme_bg_color; + } + if ui.button("Pick Relays Again") + .on_hover_cursor(egui::CursorIcon::PointingHand) + .clicked() { + let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PickRelays); + } + }); }); ui.add_space(10.0); ui.horizontal_wrapped(|ui| { @@ -69,8 +131,6 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr app.set_page(Page::Settings); } }); - ui.add_space(10.0); - if GLOBALS.relay_picker.pubkey_counts_iter().count() > 0 { ui.label( format!("The Relay-Picker has tried to connect to at least {} relays \ @@ -78,11 +138,6 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr You can manually ask the Relay-Picker to pick again, however most of the time it has already \ tried its best.", app.settings.num_relays_per_person)); - ui.add_space(10.0); - if ui.link("Pick Again").clicked() { - let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PickRelays); - } - ui.add_space(10.0); let id_source = ui.auto_id_with("relay-coverage-scroll"); egui::ScrollArea::vertical() @@ -91,12 +146,20 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr for elem in GLOBALS.relay_picker.pubkey_counts_iter() { let pk = elem.key(); let count = elem.value(); + let name = GossipUi::display_name_from_pubkey_lookup(pk); + let relays = find_relays_for_pubkey(pk); + let hover_text = format!("Go to profile of {}", name); - let entry = CoverageEntry::new(pk, count); - if entry.show(ui).clicked() { + let entry = CoverageEntry::new(pk, name, count, relays); + if entry.show(ui, app) + .on_hover_text(hover_text) + .on_hover_cursor(egui::CursorIcon::PointingHand) + .clicked() + { app.set_page(Page::Person(*pk)); } } + // uncomment below to mock with people entries for development // for pk in GLOBALS.people.get_followed_pubkeys() { // let entry = CoverageEntry::new(&pk, &0); // if entry.show(ui).clicked() { diff --git a/src/ui/widgets/list_entry.rs b/src/ui/widgets/list_entry.rs index bc3fbce5..a1bc5008 100644 --- a/src/ui/widgets/list_entry.rs +++ b/src/ui/widgets/list_entry.rs @@ -17,6 +17,8 @@ pub(crate) const TEXT_LEFT: f32 = 20.0; pub(crate) const TEXT_RIGHT: f32 = 25.0; /// Start of text (excl. outer margin): top pub(crate) const TEXT_TOP: f32 = 15.0; +/// Title font size +pub(crate) const TITLE_FONT_SIZE: f32 = 16.5; /// Thickness of separator const HLINE_THICKNESS: f32 = 1.5; @@ -27,12 +29,12 @@ pub(crate) fn allocate_space(ui: &mut Ui, height: f32) -> (Rect, Response) { ui.allocate_exact_size(vec2(available_width, height), Sense::hover()) } -pub(crate) fn paint_frame(ui: &mut Ui, rect: &Rect) { +pub(crate) fn paint_frame(ui: &mut Ui, rect: &Rect, fill: Option) { let frame_rect = Rect::from_min_max( rect.min + vec2(OUTER_MARGIN_LEFT, OUTER_MARGIN_TOP), rect.max - vec2(OUTER_MARGIN_RIGHT, OUTER_MARGIN_BOTTOM), ); - let fill = ui.style().visuals.extreme_bg_color; + let fill = fill.unwrap_or(ui.style().visuals.extreme_bg_color); ui.painter().add(epaint::RectShape { rect: frame_rect, rounding: Rounding::same(5.0), diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs index bc1fc2cf..ad1dc23d 100644 --- a/src/ui/widgets/mod.rs +++ b/src/ui/widgets/mod.rs @@ -21,6 +21,9 @@ pub use relay_entry::{RelayEntry, RelayEntryView}; // ui.label(job.job); // } +/// Copy symbol for copy button +pub(crate) const COPY_SYMBOL: &str = "\u{1F4CB}"; + pub fn break_anywhere_hyperlink_to(ui: &mut Ui, text: impl Into, url: impl ToString) { let mut job = text.into().into_text_job( ui.style(), diff --git a/src/ui/widgets/relay_entry.rs b/src/ui/widgets/relay_entry.rs index 6cdd7529..e53702cc 100644 --- a/src/ui/widgets/relay_entry.rs +++ b/src/ui/widgets/relay_entry.rs @@ -43,8 +43,6 @@ const USAGE_LINE_X_END: f32 = -10.0; const USAGE_LINE_THICKNESS: f32 = 1.0; /// Spacing between nip11 text rows const NIP11_Y_SPACING: f32 = 20.0; -/// Copy symbol for nip11 items copy button -const COPY_SYMBOL: &str = "\u{2398}"; /// Status symbol for status color indicator const STATUS_SYMBOL: &str = "\u{25CF}"; /// Space reserved for status symbol before title @@ -223,7 +221,7 @@ impl RelayEntry { if self.relay.url.0.len() > TITLE_MAX_LEN { title.push('\u{2026}'); // append ellipsis } - let text = RichText::new(title).size(16.5); + let text = RichText::new(title).size(list_entry::TITLE_FONT_SIZE); let pos = rect.min + vec2(TEXT_LEFT + STATUS_SYMBOL_SPACE, TEXT_TOP); let rect = draw_text_at(ui, pos, text.into(), Align::LEFT, Some(self.accent), None); ui.interact(rect, ui.next_auto_id(), Sense::hover()) @@ -601,7 +599,7 @@ impl RelayEntry { let rect = draw_text_at(ui, pos, contact.into(), align, None, None); let id = self.make_id("copy_nip11_contact"); let pos = pos + vec2(rect.width() + ui.spacing().item_spacing.x, 0.0); - let text = RichText::new(COPY_SYMBOL); + let text = RichText::new(crate::ui::widgets::COPY_SYMBOL); let (galley, response) = allocate_text_at(ui, pos, text.into(), align, id); if response.clicked() { ui.output_mut(|o| { @@ -634,7 +632,7 @@ impl RelayEntry { let rect = draw_text_at(ui, pos, npub.clone().into(), align, None, None); let id = self.make_id("copy_nip11_npub"); let pos = pos + vec2(rect.width() + ui.spacing().item_spacing.x, 0.0); - let text = RichText::new(COPY_SYMBOL); + let text = RichText::new(crate::ui::widgets::COPY_SYMBOL); let (galley, response) = allocate_text_at(ui, pos, text.into(), align, id); if response.clicked() { ui.output_mut(|o| { @@ -1035,7 +1033,7 @@ impl RelayEntry { // all the heavy lifting is only done if it's actually visible if ui.is_rect_visible(rect) { - list_entry::paint_frame(ui, &rect); + list_entry::paint_frame(ui, &rect, None); self.paint_title(ui, &rect); response |= self.paint_edit_btn(ui, &rect); if self.relay.usage_bits != 0 { @@ -1052,7 +1050,7 @@ impl RelayEntry { // all the heavy lifting is only done if it's actually visible if ui.is_rect_visible(rect) { - list_entry::paint_frame(ui, &rect); + list_entry::paint_frame(ui, &rect, None); self.paint_title(ui, &rect); response |= self.paint_edit_btn(ui, &rect); self.paint_stats(ui, &rect); @@ -1070,7 +1068,7 @@ impl RelayEntry { // all the heavy lifting is only done if it's actually visible if ui.is_rect_visible(rect) { - list_entry::paint_frame(ui, &rect); + list_entry::paint_frame(ui, &rect, None); self.paint_title(ui, &rect); self.paint_stats(ui, &rect); paint_hline(ui, &rect, HLINE_1_Y_OFFSET);