From e801abbe582fa6471fdd56678dd6678e330b7f15 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Wed, 6 Dec 2023 16:05:50 -0600 Subject: [PATCH 1/8] - Lists: Apply general list_entry style - Harmonize more menu feature to more_menu.rs --- gossip-bin/src/ui/feed/post.rs | 2 +- gossip-bin/src/ui/mod.rs | 30 ++- gossip-bin/src/ui/people/list.rs | 343 +++++++++++++++---------- gossip-bin/src/ui/people/mod.rs | 2 + gossip-bin/src/ui/relays/active.rs | 6 +- gossip-bin/src/ui/relays/known.rs | 6 +- gossip-bin/src/ui/relays/mine.rs | 6 +- gossip-bin/src/ui/relays/mod.rs | 92 ++----- gossip-bin/src/ui/widgets/mod.rs | 3 + gossip-bin/src/ui/widgets/more_menu.rs | 117 +++++++++ 10 files changed, 381 insertions(+), 226 deletions(-) create mode 100644 gossip-bin/src/ui/widgets/more_menu.rs diff --git a/gossip-bin/src/ui/feed/post.rs b/gossip-bin/src/ui/feed/post.rs index abcd9905..0e86a87f 100644 --- a/gossip-bin/src/ui/feed/post.rs +++ b/gossip-bin/src/ui/feed/post.rs @@ -565,7 +565,7 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram app.draft_data.draft.push(emoji); } }); - ui.add_space(20.0); + btn_h_space!(ui); if ui .button(RichText::new("🥩")) .on_hover_text("raw content preview") diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index a12cc913..7acbc019 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -10,6 +10,12 @@ macro_rules! text_edit_multiline { }; } +macro_rules! btn_h_space { + ($ui:ident) => { + $ui.add_space(20.0) + }; +} + mod components; mod dm_chat_list; mod feed; @@ -352,6 +358,9 @@ struct GossipUi { // RelayUi relays: relays::RelayUi, + // people::ListUi + people_list: people::ListUi, + // Post rendering render_raw: Option, render_qr: Option, @@ -401,10 +410,8 @@ struct GossipUi { delegatee_tag_str: String, // User entry: general - entering_follow_someone_on_list: bool, follow_someone: String, add_relay: String, // dep - clear_list_needs_confirm: bool, password: String, password2: String, password3: String, @@ -607,6 +614,7 @@ impl GossipUi { qr_codes: HashMap::new(), notes: Notes::new(), relays: relays::RelayUi::new(), + people_list: people::ListUi::new(), render_raw: None, render_qr: None, approved: HashSet::new(), @@ -644,10 +652,8 @@ impl GossipUi { editing_metadata: false, metadata: Metadata::new(), delegatee_tag_str: "".to_owned(), - entering_follow_someone_on_list: false, follow_someone: "".to_owned(), add_relay: "".to_owned(), - clear_list_needs_confirm: false, password: "".to_owned(), password2: "".to_owned(), password3: "".to_owned(), @@ -1331,6 +1337,22 @@ impl GossipUi { } } + pub fn richtext_from_person_nip05(person: &Person) -> RichText { + if let Some(mut nip05) = person.nip05().map(|s| s.to_owned()) { + if nip05.starts_with("_@") { + nip05 = nip05.get(2..).unwrap().to_string(); + } + + if person.nip05_valid { + RichText::new(nip05).monospace() + } else { + RichText::new(nip05).monospace().strikethrough() + } + } else { + RichText::default() + } + } + pub fn render_person_name_line( app: &mut GossipUi, ui: &mut Ui, diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index aa6cfe55..ad536e9d 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -7,6 +7,22 @@ use gossip_lib::comms::ToOverlordMessage; use gossip_lib::{Person, PersonList, GLOBALS}; use nostr_types::{Profile, PublicKey}; +pub(crate) struct ListUi { + configure_list_menu_active: bool, + entering_follow_someone_on_list: bool, + clear_list_needs_confirm: bool, +} + +impl ListUi { + pub(crate) fn new() -> Self { + Self { + configure_list_menu_active: false, + entering_follow_someone_on_list: false, + clear_list_needs_confirm: false, + } + } +} + pub(super) fn update( app: &mut GossipUi, ctx: &Context, @@ -14,6 +30,8 @@ pub(super) fn update( ui: &mut Ui, list: PersonList, ) { + // prepare data + // TODO cache this to improve performance let people = { let members = GLOBALS.storage.get_people_in_list(list).unwrap_or_default(); @@ -32,8 +50,6 @@ pub(super) fn update( people }; - ui.add_space(12.0); - let latest_event_data = GLOBALS .people .latest_person_list_event_data @@ -50,7 +66,7 @@ pub(super) fn update( } } - let txt = if let Some(private_len) = latest_event_data.private_len { + let remote_text = if let Some(private_len) = latest_event_data.private_len { format!( "REMOTE: {} (public_len={} private_len={})", asof, latest_event_data.public_len, private_len @@ -62,76 +78,6 @@ pub(super) fn update( ) }; - ui.label(RichText::new(txt).size(15.0)) - .on_hover_text("This is the data in the latest list event fetched from relays"); - - ui.add_space(10.0); - - ui.horizontal(|ui| { - ui.add_space(30.0); - - if GLOBALS.signer.is_ready() { - if ui - .button("↓ Overwrite ↓") - .on_hover_text( - "This imports data from the latest event, erasing anything that is already here", - ) - .clicked() - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::UpdatePersonList { - person_list: list, - merge: false, - }); - } - if ui - .button("↓ Merge ↓") - .on_hover_text( - "This imports data from the latest event, merging it into what is already here", - ) - .clicked() - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::UpdatePersonList { - person_list: list, - merge: true, - }); - } - - if ui - .button("↑ Publish ↑") - .on_hover_text("This publishes the list to your relays") - .clicked() - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::PushPersonList(list)); - } - } - - if GLOBALS.signer.is_ready() { - if app.clear_list_needs_confirm { - if ui.button("CANCEL").clicked() { - app.clear_list_needs_confirm = false; - } - if ui.button("YES, CLEAR ALL").clicked() { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::ClearPersonList(list)); - app.clear_list_needs_confirm = false; - } - } else { - if ui.button("Clear All").clicked() { - app.clear_list_needs_confirm = true; - } - } - } - }); - - ui.add_space(10.0); - let last_list_edit = match GLOBALS.storage.get_person_list_last_edit_time(list) { Ok(Some(date)) => date, Ok(None) => 0, @@ -149,12 +95,90 @@ pub(super) fn update( ledit = formatted; } } - ui.label(RichText::new(format!("LOCAL: {} (size={})", ledit, people.len())).size(15.0)) - .on_hover_text("This is the local (and effective) list"); - if !GLOBALS.signer.is_ready() { - ui.add_space(10.0); - ui.horizontal_wrapped(|ui| { + // render page + widgets::page_header(ui, format!("{} ({})", list.name(), people.len()), |ui| { + ui.add_enabled_ui(true, |ui| { + let min_size = vec2(50.0, 20.0); + + widgets::MoreMenu::new(&app) + .with_min_size(min_size) + .show(ui, &mut app.people_list.configure_list_menu_active, |ui|{ + // since we are displaying over an accent color background, load that style + app.theme.accent_button_2_style(ui.style_mut()); + + if ui.button("Clear All").clicked() { + app.people_list.clear_list_needs_confirm = true; + } + + // ui.add_space(8.0); + }); + }); + + btn_h_space!(ui); + + if ui.button("Add contact").clicked() { + app.people_list.entering_follow_someone_on_list = true; + } + }); + + if GLOBALS.signer.is_ready() { + ui.vertical(|ui| { + ui.label(RichText::new(remote_text)) + .on_hover_text("This is the data in the latest list event fetched from relays"); + + ui.add_space(5.0); + + // remote <-> local buttons + ui.horizontal(|ui|{ + if ui + .button("↓ Overwrite ↓") + .on_hover_text( + "This imports data from the latest event, erasing anything that is already here", + ) + .clicked() + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::UpdatePersonList { + person_list: list, + merge: false, + }); + } + if ui + .button("↓ Merge ↓") + .on_hover_text( + "This imports data from the latest event, merging it into what is already here", + ) + .clicked() + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::UpdatePersonList { + person_list: list, + merge: true, + }); + } + + if ui + .button("↑ Publish ↑") + .on_hover_text("This publishes the list to your relays") + .clicked() + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::PushPersonList(list)); + } + }); + + ui.add_space(5.0); + + // local timestamp + ui.label(RichText::new(format!("LOCAL: {} (size={})", ledit, people.len()))) + .on_hover_text("This is the local (and effective) list"); + }); + } else { + ui.horizontal(|ui| { ui.label("You need to "); if ui.link("setup your identity").clicked() { app.set_page(ctx, Page::YourKeys); @@ -163,73 +187,114 @@ pub(super) fn update( }); } - ui.add_space(10.0); - - if ui.button("Follow New").clicked() { - app.entering_follow_someone_on_list = true; - } - - ui.separator(); - ui.add_space(10.0); - - ui.heading(format!("{} ({})", list.name(), people.len())); - ui.add_space(14.0); - - ui.separator(); - - app.vert_scroll_area().show(ui, |ui| { - for (person, public) in people.iter() { - 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 widgets::paint_avatar(ui, person, &avatar, widgets::AvatarSize::Feed).clicked() { - app.set_page(ctx, Page::Person(person.pubkey)); - }; - - ui.vertical(|ui| { - ui.label(RichText::new(gossip_lib::names::pubkey_short(&person.pubkey)).weak()); - GossipUi::render_person_name_line(app, ui, person, false); - if !GLOBALS - .storage - .have_persons_relays(person.pubkey) - .unwrap_or(false) - { - ui.label( - RichText::new("Relay list not found") - .color(app.theme.warning_marker_text_color()), - ); - } - + if app.people_list.clear_list_needs_confirm { + const DLG_SIZE: Vec2 = vec2(250.0, 40.0); + if widgets::modal_popup(ui, DLG_SIZE, |ui| { + ui.vertical(|ui| { + ui.label("Are you sure you want to clear this list?"); + ui.add_space(10.0); + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT),|ui| { ui.horizontal(|ui| { - if crate::ui::components::switch_simple(ui, *public).clicked() { - let _ = GLOBALS.storage.add_person_to_list( - &person.pubkey, - list, - !*public, - None, - ); + if ui.button("Cancel").clicked() { + app.people_list.clear_list_needs_confirm = false; } - ui.label(if *public { "public" } else { "private" }); + ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui|{ + if ui.button("YES, CLEAR ALL").clicked() { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::ClearPersonList(list)); + app.people_list.clear_list_needs_confirm = false; + } + }); }); }); }); + }).inner.clicked() { + app.people_list.clear_list_needs_confirm = false; + } + } - if ui.button("Remove").clicked() { - let _ = GLOBALS - .storage - .remove_person_from_list(&person.pubkey, list, None); + ui.add_space(10.0); + + app.vert_scroll_area().show(ui, |ui| { + for (person, public) in people.iter() { + let row_response = widgets::list_entry::make_frame(ui) + .show(ui, |ui| { + ui.horizontal(|ui| { + // Avatar first + let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) { + avatar + } else { + app.placeholder_avatar.clone() + }; + let avatar_height = widgets::paint_avatar(ui, person, &avatar, widgets::AvatarSize::Feed).rect.height(); + + ui.add_space(20.0); + + ui.vertical(|ui| { + ui.set_min_height(avatar_height); + ui.horizontal(|ui| { + ui.label(GossipUi::person_name(person)); + + ui.add_space(10.0); + + if !GLOBALS + .storage + .have_persons_relays(person.pubkey) + .unwrap_or(false) + { + ui.label( + RichText::new("Relay list not found") + .color(app.theme.warning_marker_text_color()), + ); + } + }); + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { + ui.horizontal(|ui| { + ui.label(RichText::new(gossip_lib::names::pubkey_short(&person.pubkey)).weak()); + + ui.add_space(10.0); + + ui.label(GossipUi::richtext_from_person_nip05(person)); + }); + }); + }); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + ui.set_min_height(avatar_height); + // actions + if ui.link("Remove").clicked() { + let _ = GLOBALS + .storage + .remove_person_from_list(&person.pubkey, list, None); + } + + ui.add_space(20.0); + + // private / public switch + if crate::ui::components::switch_simple(ui, *public).clicked() { + let _ = GLOBALS.storage.add_person_to_list( + &person.pubkey, + list, + !*public, + None, + ); + } + ui.label(if *public { "public" } else { "private" }); + }); + }); + }); + if row_response + .response + .interact(egui::Sense::click()) + .on_hover_cursor(egui::CursorIcon::PointingHand) + .clicked() { + app.set_page(ctx, Page::Person(person.pubkey)); } - - ui.add_space(4.0); - ui.separator(); } }); - if app.entering_follow_someone_on_list { + if app.people_list.entering_follow_someone_on_list { const DLG_SIZE: Vec2 = vec2(400.0, 200.0); let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, |ui| { ui.heading("Follow someone"); @@ -248,14 +313,14 @@ pub(super) fn update( let _ = GLOBALS .to_overlord .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); - app.entering_follow_someone_on_list = false; + app.people_list.entering_follow_someone_on_list = false; } else if let Ok(pubkey) = PublicKey::try_from_hex_string(app.follow_someone.trim(), true) { let _ = GLOBALS .to_overlord .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); - app.entering_follow_someone_on_list = false; + app.people_list.entering_follow_someone_on_list = false; } else if let Ok(profile) = Profile::try_from_bech32_string(app.follow_someone.trim(), true) { @@ -264,7 +329,7 @@ pub(super) fn update( list, true, )); - app.entering_follow_someone_on_list = false; + app.people_list.entering_follow_someone_on_list = false; } else if gossip_lib::nip05::parse_nip05(app.follow_someone.trim()).is_ok() { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNip05( app.follow_someone.trim().to_owned(), @@ -281,7 +346,7 @@ pub(super) fn update( } }); if ret.inner.clicked() { - app.entering_follow_someone_on_list = false; + app.people_list.entering_follow_someone_on_list = false; } } } diff --git a/gossip-bin/src/ui/people/mod.rs b/gossip-bin/src/ui/people/mod.rs index be557840..684778f6 100644 --- a/gossip-bin/src/ui/people/mod.rs +++ b/gossip-bin/src/ui/people/mod.rs @@ -6,6 +6,8 @@ mod list; mod lists; mod person; +pub(crate) use list::ListUi; + pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { if app.page == Page::PeopleLists { lists::update(app, ctx, _frame, ui); diff --git a/gossip-bin/src/ui/relays/active.rs b/gossip-bin/src/ui/relays/active.rs index 6208a7d0..6f89f73b 100644 --- a/gossip-bin/src/ui/relays/active.rs +++ b/gossip-bin/src/ui/relays/active.rs @@ -15,11 +15,11 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra widgets::page_header(ui, Page::RelaysActivityMonitor.name(), |ui| { ui.set_enabled(!is_editing); super::configure_list_btn(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); super::relay_filter_combo(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); super::relay_sort_combo(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); widgets::search_filter_field(ui, &mut app.relays.search, 200.0); ui.add_space(200.0); // search_field somehow doesn't "take up" space if ui diff --git a/gossip-bin/src/ui/relays/known.rs b/gossip-bin/src/ui/relays/known.rs index f403548f..9d69a5d6 100644 --- a/gossip-bin/src/ui/relays/known.rs +++ b/gossip-bin/src/ui/relays/known.rs @@ -11,11 +11,11 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr widgets::page_header(ui, Page::RelaysKnownNetwork.name(), |ui| { ui.set_enabled(!is_editing); super::configure_list_btn(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); super::relay_filter_combo(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); super::relay_sort_combo(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); widgets::search_filter_field(ui, &mut app.relays.search, 200.0); }); diff --git a/gossip-bin/src/ui/relays/mine.rs b/gossip-bin/src/ui/relays/mine.rs index 2a765c02..fa412e68 100644 --- a/gossip-bin/src/ui/relays/mine.rs +++ b/gossip-bin/src/ui/relays/mine.rs @@ -12,11 +12,11 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr widgets::page_header(ui, Page::RelaysMine.name(), |ui| { ui.set_enabled(!is_editing); super::configure_list_btn(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); super::relay_filter_combo(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); super::relay_sort_combo(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); widgets::search_filter_field(ui, &mut app.relays.search, 200.0); ui.add_space(200.0); // search_field somehow doesn't "take up" space widgets::set_important_button_visuals(ui, app); diff --git a/gossip-bin/src/ui/relays/mod.rs b/gossip-bin/src/ui/relays/mod.rs index 4f0a9943..83a3432a 100644 --- a/gossip-bin/src/ui/relays/mod.rs +++ b/gossip-bin/src/ui/relays/mod.rs @@ -1,7 +1,7 @@ use std::cmp::Ordering; use super::{widgets, GossipUi, Page}; -use eframe::{egui, epaint::PathShape}; +use eframe::egui; use egui::{Context, Ui}; use egui_winit::egui::{vec2, Id, Rect, RichText}; use gossip_lib::{comms::ToOverlordMessage, Relay, GLOBALS}; @@ -448,83 +448,29 @@ fn entry_dialog_step2(ui: &mut Ui, app: &mut GossipUi) { /// Draw button with configure popup /// pub(super) fn configure_list_btn(app: &mut GossipUi, ui: &mut Ui) { - let (response, painter) = ui.allocate_painter(vec2(20.0, 20.0), egui::Sense::click()); - let response = response.on_hover_cursor(egui::CursorIcon::PointingHand); - let response = if !app.relays.configure_list_menu_active { - response.on_hover_text("Configure List View") - } else { - response - }; - let btn_rect = response.rect; - let color = if response.hovered() { - app.theme.accent_color() - } else { - ui.visuals().text_color() - }; - let mut mesh = egui::Mesh::with_texture((&app.options_symbol).into()); - mesh.add_rect_with_uv( - btn_rect.shrink(2.0), - Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), - color, - ); - painter.add(egui::Shape::mesh(mesh)); + ui.add_enabled_ui(true, |ui| { + let min_size = vec2(180.0, 20.0); - if response.clicked() { - app.relays.configure_list_menu_active ^= true; - } + widgets::MoreMenu::new(app) + .with_min_size(min_size) + .with_hover_text("Configure List View".to_owned()) + .show(ui,&mut app.relays.configure_list_menu_active, |ui|{ + let size = ui.spacing().interact_size.y * egui::vec2(1.6, 0.8); - let button_center_bottom = response.rect.center_bottom(); - let seen_on_popup_position = button_center_bottom + vec2(-180.0, widgets::DROPDOWN_DISTANCE); + // since we are displaying over an accent color background, load that style + app.theme.on_accent_style(ui.style_mut()); - let id: Id = "configure-list-menu".into(); - let mut frame = egui::Frame::popup(ui.style()); - let area = egui::Area::new(id) - .movable(false) - .interactable(true) - .order(egui::Order::Foreground) - .fixed_pos(seen_on_popup_position) - .constrain(true); - if app.relays.configure_list_menu_active { - let menuresp = area.show(ui.ctx(), |ui| { - frame.fill = app.theme.accent_color(); - frame.stroke = egui::Stroke::NONE; - // frame.shadow = egui::epaint::Shadow::NONE; - frame.rounding = egui::Rounding::same(5.0); - frame.inner_margin = egui::Margin::symmetric(20.0, 16.0); - frame.show(ui, |ui| { - let path = PathShape::convex_polygon( - [ - button_center_bottom, - button_center_bottom - + vec2(widgets::DROPDOWN_DISTANCE, widgets::DROPDOWN_DISTANCE), - button_center_bottom - + vec2(-widgets::DROPDOWN_DISTANCE, widgets::DROPDOWN_DISTANCE), - ] - .to_vec(), - app.theme.accent_color(), - egui::Stroke::NONE, - ); - ui.painter().add(path); - let size = ui.spacing().interact_size.y * egui::vec2(1.6, 0.8); - - // since we are displaying over an accent color background, load that style - app.theme.on_accent_style(ui.style_mut()); - - ui.horizontal(|ui| { - crate::ui::components::switch_with_size(ui, &mut app.relays.show_details, size); - ui.label("Show details"); - }); - ui.add_space(8.0); - ui.horizontal(|ui| { - crate::ui::components::switch_with_size(ui, &mut app.relays.show_hidden, size); - ui.label("Show hidden relays"); - }); + ui.horizontal(|ui| { + crate::ui::components::switch_with_size(ui, &mut app.relays.show_details, size); + ui.label("Show details"); + }); + ui.add_space(8.0); + ui.horizontal(|ui| { + crate::ui::components::switch_with_size(ui, &mut app.relays.show_hidden, size); + ui.label("Show hidden relays"); }); }); - if menuresp.response.clicked_elsewhere() && !response.clicked() { - app.relays.configure_list_menu_active = false; - } - } + }); } /// diff --git a/gossip-bin/src/ui/widgets/mod.rs b/gossip-bin/src/ui/widgets/mod.rs index 4a33d3d5..d305507c 100644 --- a/gossip-bin/src/ui/widgets/mod.rs +++ b/gossip-bin/src/ui/widgets/mod.rs @@ -1,6 +1,9 @@ mod avatar; pub(crate) use avatar::{paint_avatar, AvatarSize}; +mod more_menu; +pub(super) use more_menu::MoreMenu; + mod copy_button; pub(crate) mod list_entry; pub use copy_button::{CopyButton, COPY_SYMBOL_SIZE}; diff --git a/gossip-bin/src/ui/widgets/more_menu.rs b/gossip-bin/src/ui/widgets/more_menu.rs new file mode 100644 index 00000000..3e665a38 --- /dev/null +++ b/gossip-bin/src/ui/widgets/more_menu.rs @@ -0,0 +1,117 @@ +use eframe::epaint::PathShape; +use egui_winit::egui::{Ui, self, vec2, Rect, Id, Vec2, TextureHandle, Color32}; + +use crate::ui::GossipUi; + +pub(in crate::ui) struct MoreMenu { + id: Id, + min_size: Vec2, + hover_text: Option, + accent_color: Color32, + options_symbol: TextureHandle, +} + +impl MoreMenu { + pub fn new(app: &GossipUi) -> Self { + Self { + id: "more-menu".into(), + min_size: Vec2 { x: 0.0, y: 0.0 }, + hover_text: None, + accent_color: app.theme.accent_color(), + options_symbol: app.options_symbol.clone(), + } + } + + #[allow(unused)] + pub fn with_id(mut self, id: impl Into) -> Self { + self.id = id.into(); + self + } + + #[allow(unused)] + pub fn with_min_size(mut self, min_size: Vec2) -> Self { + self.min_size = min_size; + self + } + + #[allow(unused)] + pub fn with_hover_text(mut self, text: String) -> Self { + self.hover_text = Some(text); + self + } + + pub fn show(&self, ui: &mut Ui, active: &mut bool, content: impl FnOnce(&mut Ui) ) { + let (response, painter) = ui.allocate_painter(vec2(20.0, 20.0), egui::Sense::click()); + let response = response.on_hover_cursor(egui::CursorIcon::PointingHand); + let response = if let Some(text) = &self.hover_text { + if !*active { + response.on_hover_text(text) + } else { + response + } + } else { + response + }; + let btn_rect = response.rect; + let color = if response.hovered() { + self.accent_color + } else { + ui.visuals().text_color() + }; + let mut mesh = egui::Mesh::with_texture((&self.options_symbol).into()); + mesh.add_rect_with_uv( + btn_rect.shrink(2.0), + Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), + color, + ); + painter.add(egui::Shape::mesh(mesh)); + + if response.clicked() { + *active ^= true; + } + + let button_center_bottom = response.rect.center_bottom(); + let seen_on_popup_position = button_center_bottom + vec2(-(self.min_size.x - 2.0*super::DROPDOWN_DISTANCE), super::DROPDOWN_DISTANCE); + + let mut frame = egui::Frame::popup(ui.style()); + let area = egui::Area::new(self.id) + .movable(false) + .interactable(true) + .order(egui::Order::Foreground) + .fixed_pos(seen_on_popup_position) + .constrain(true); + if *active { + let menuresp = area.show(ui.ctx(), |ui| { + frame.fill = self.accent_color; + frame.stroke = egui::Stroke::NONE; + // frame.shadow = egui::epaint::Shadow::NONE; + frame.rounding = egui::Rounding::same(5.0); + frame.inner_margin = egui::Margin::symmetric(20.0, 16.0); + frame.show(ui, |ui| { + ui.set_min_size(self.min_size); + let path = PathShape::convex_polygon( + [ + button_center_bottom, + button_center_bottom + + vec2(super::DROPDOWN_DISTANCE, super::DROPDOWN_DISTANCE), + button_center_bottom + + vec2(-super::DROPDOWN_DISTANCE, super::DROPDOWN_DISTANCE), + ] + .to_vec(), + self.accent_color, + egui::Stroke::NONE, + ); + ui.painter().add(path); + + // now show menu content + content(ui); + }); + }); + if menuresp.response.clicked_elsewhere() && !response.clicked() { + *active = false; + } + } + } +} + + From fc982c8ed419349c0a2451aea3f34e9c98863fbe Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 7 Dec 2023 10:21:02 -0600 Subject: [PATCH 2/8] People List: Cache list and limit refresh to every 1 sec, greatly improves scrolling smoothness --- gossip-bin/src/ui/mod.rs | 1 + gossip-bin/src/ui/people/list.rs | 258 ++++++++++++++++++------------- gossip-bin/src/ui/people/mod.rs | 10 ++ 3 files changed, 164 insertions(+), 105 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index 7acbc019..c81f61ec 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -735,6 +735,7 @@ impl GossipUi { self.close_all_menus(ctx); } Page::PeopleLists => { + people::enter_page(self); self.close_all_menus(ctx); } Page::Person(pubkey) => { diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index ad536e9d..3e54c0ba 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -1,3 +1,5 @@ +use std::time::{Instant, Duration}; + use super::{GossipUi, Page}; use crate::ui::widgets; use eframe::egui; @@ -8,6 +10,13 @@ use gossip_lib::{Person, PersonList, GLOBALS}; use nostr_types::{Profile, PublicKey}; pub(crate) struct ListUi { + // cache + cache_last_list: Option, + cache_next_refresh: Instant, + cache_people: Vec<(Person, bool)>, + cache_remote_tag: String, + cache_local_tag: String, + configure_list_menu_active: bool, entering_follow_someone_on_list: bool, clear_list_needs_confirm: bool, @@ -16,6 +25,12 @@ pub(crate) struct ListUi { impl ListUi { pub(crate) fn new() -> Self { Self { + // cache + cache_last_list: None, + cache_next_refresh: Instant::now(), + cache_people: Vec::new(), + cache_remote_tag: String::new(), + cache_local_tag: String::new(), configure_list_menu_active: false, entering_follow_someone_on_list: false, clear_list_needs_confirm: false, @@ -23,6 +38,10 @@ impl ListUi { } } +pub(super) fn enter_page(app: &mut GossipUi, list: PersonList) { + refresh_list_data(app, list); +} + pub(super) fn update( app: &mut GossipUi, ctx: &Context, @@ -30,74 +49,14 @@ pub(super) fn update( ui: &mut Ui, list: PersonList, ) { - // prepare data - // TODO cache this to improve performance - let people = { - let members = GLOBALS.storage.get_people_in_list(list).unwrap_or_default(); - - let mut people: Vec<(Person, bool)> = Vec::new(); - - for (pk, public) in &members { - if let Ok(Some(person)) = GLOBALS.storage.read_person(pk) { - people.push((person, *public)); - } else { - let person = Person::new(pk.to_owned()); - let _ = GLOBALS.storage.write_person(&person, None); - people.push((person, *public)); - } - } - people.sort_by(|a, b| a.0.cmp(&b.0)); - people - }; - - let latest_event_data = GLOBALS - .people - .latest_person_list_event_data - .get(&list) - .map(|v| v.value().clone()) - .unwrap_or_default(); - - let mut asof = "unknown".to_owned(); - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(latest_event_data.when.0) { - if let Ok(formatted) = stamp.format(time::macros::format_description!( - "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" - )) { - asof = formatted; - } - } - - let remote_text = if let Some(private_len) = latest_event_data.private_len { - format!( - "REMOTE: {} (public_len={} private_len={})", - asof, latest_event_data.public_len, private_len - ) - } else { - format!( - "REMOTE: {} (public_len={})", - asof, latest_event_data.public_len - ) - }; - - let last_list_edit = match GLOBALS.storage.get_person_list_last_edit_time(list) { - Ok(Some(date)) => date, - Ok(None) => 0, - Err(e) => { - tracing::error!("{}", e); - 0 - } - }; - - let mut ledit = "unknown".to_owned(); - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_list_edit) { - if let Ok(formatted) = stamp.format(time::macros::format_description!( - "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" - )) { - ledit = formatted; - } + if app.people_list.cache_next_refresh < Instant::now() || + app.people_list.cache_last_list.is_none() || + app.people_list.cache_last_list.unwrap() != list { + refresh_list_data(app, list); } // render page - widgets::page_header(ui, format!("{} ({})", list.name(), people.len()), |ui| { + widgets::page_header(ui, format!("{} ({})", list.name(), app.people_list.cache_people.len()), |ui| { ui.add_enabled_ui(true, |ui| { let min_size = vec2(50.0, 20.0); @@ -124,7 +83,7 @@ pub(super) fn update( if GLOBALS.signer.is_ready() { ui.vertical(|ui| { - ui.label(RichText::new(remote_text)) + ui.label(RichText::new(&app.people_list.cache_remote_tag)) .on_hover_text("This is the data in the latest list event fetched from relays"); ui.add_space(5.0); @@ -174,7 +133,7 @@ pub(super) fn update( ui.add_space(5.0); // local timestamp - ui.label(RichText::new(format!("LOCAL: {} (size={})", ledit, people.len()))) + ui.label(RichText::new(&app.people_list.cache_local_tag)) .on_hover_text("This is the local (and effective) list"); }); } else { @@ -217,6 +176,8 @@ pub(super) fn update( ui.add_space(10.0); app.vert_scroll_area().show(ui, |ui| { + // not nice but needed because of 'app' borrow in closure + let people = app.people_list.cache_people.clone(); for (person, public) in people.iter() { let row_response = widgets::list_entry::make_frame(ui) .show(ui, |ui| { @@ -279,6 +240,7 @@ pub(super) fn update( !*public, None, ); + mark_refresh(app); } ui.label(if *public { "public" } else { "private" }); }); @@ -297,8 +259,12 @@ pub(super) fn update( if app.people_list.entering_follow_someone_on_list { const DLG_SIZE: Vec2 = vec2(400.0, 200.0); let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, |ui| { + // TODO use tagging search here + ui.heading("Follow someone"); + ui.add_space(8.0); + ui.horizontal(|ui| { ui.label("Enter"); ui.add( @@ -306,47 +272,129 @@ pub(super) fn update( .hint_text("npub1, hex key, nprofile1, or user@domain"), ); }); - if ui.button("follow").clicked() { - if let Ok(pubkey) = - PublicKey::try_from_bech32_string(app.follow_someone.trim(), true) - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); - app.people_list.entering_follow_someone_on_list = false; - } else if let Ok(pubkey) = - PublicKey::try_from_hex_string(app.follow_someone.trim(), true) - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); - app.people_list.entering_follow_someone_on_list = false; - } else if let Ok(profile) = - Profile::try_from_bech32_string(app.follow_someone.trim(), true) - { - let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNprofile( - profile.clone(), - list, - true, - )); - app.people_list.entering_follow_someone_on_list = false; - } else if gossip_lib::nip05::parse_nip05(app.follow_someone.trim()).is_ok() { - let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNip05( - app.follow_someone.trim().to_owned(), - list, - true, - )); - } else { - GLOBALS - .status_queue - .write() - .write("Invalid pubkey.".to_string()); - } - app.follow_someone = "".to_owned(); - } + + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { + ui.horizontal(|ui| { + if ui.button("follow").clicked() { + if let Ok(pubkey) = + PublicKey::try_from_bech32_string(app.follow_someone.trim(), true) + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); + app.people_list.entering_follow_someone_on_list = false; + } else if let Ok(pubkey) = + PublicKey::try_from_hex_string(app.follow_someone.trim(), true) + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); + app.people_list.entering_follow_someone_on_list = false; + } else if let Ok(profile) = + Profile::try_from_bech32_string(app.follow_someone.trim(), true) + { + let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNprofile( + profile.clone(), + list, + true, + )); + app.people_list.entering_follow_someone_on_list = false; + } else if gossip_lib::nip05::parse_nip05(app.follow_someone.trim()).is_ok() { + let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNip05( + app.follow_someone.trim().to_owned(), + list, + true, + )); + } else { + GLOBALS + .status_queue + .write() + .write("Invalid pubkey.".to_string()); + } + app.follow_someone = "".to_owned(); + } + }); + }); + }); if ret.inner.clicked() { app.people_list.entering_follow_someone_on_list = false; } } } + +fn mark_refresh(app: &mut GossipUi) { + app.people_list.cache_next_refresh = Instant::now(); +} + +fn refresh_list_data(app: &mut GossipUi, list: gossip_lib::PersonList1) { + // prepare data + app.people_list.cache_people = { + let members = GLOBALS.storage.get_people_in_list(list).unwrap_or_default(); + + let mut people: Vec<(Person, bool)> = Vec::new(); + + for (pk, public) in &members { + if let Ok(Some(person)) = GLOBALS.storage.read_person(pk) { + people.push((person, *public)); + } else { + let person = Person::new(pk.to_owned()); + let _ = GLOBALS.storage.write_person(&person, None); + people.push((person, *public)); + } + } + people.sort_by(|a, b| a.0.cmp(&b.0)); + people + }; + + let latest_event_data = GLOBALS + .people + .latest_person_list_event_data + .get(&list) + .map(|v| v.value().clone()) + .unwrap_or_default(); + + let mut asof = "unknown".to_owned(); + if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(latest_event_data.when.0) { + if let Ok(formatted) = stamp.format(time::macros::format_description!( + "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" + )) { + asof = formatted; + } + } + + app.people_list.cache_remote_tag = if let Some(private_len) = latest_event_data.private_len { + format!( + "REMOTE: {} (public_len={} private_len={})", + asof, latest_event_data.public_len, private_len + ) + } else { + format!( + "REMOTE: {} (public_len={})", + asof, latest_event_data.public_len + ) + }; + + let last_list_edit = match GLOBALS.storage.get_person_list_last_edit_time(list) { + Ok(Some(date)) => date, + Ok(None) => 0, + Err(e) => { + tracing::error!("{}", e); + 0 + } + }; + + let mut ledit = "unknown".to_owned(); + if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_list_edit) { + if let Ok(formatted) = stamp.format(time::macros::format_description!( + "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" + )) { + ledit = formatted; + } + } + + app.people_list.cache_local_tag = format!("LOCAL: {} (size={})", ledit, app.people_list.cache_people.len()); + + app.people_list.cache_next_refresh = Instant::now() + Duration::new(1, 0); + app.people_list.cache_last_list = Some(list); +} diff --git a/gossip-bin/src/ui/people/mod.rs b/gossip-bin/src/ui/people/mod.rs index 684778f6..2f104f96 100644 --- a/gossip-bin/src/ui/people/mod.rs +++ b/gossip-bin/src/ui/people/mod.rs @@ -8,6 +8,16 @@ mod person; pub(crate) use list::ListUi; +pub(super) fn enter_page(app: &mut GossipUi) { + if app.page == Page::PeopleLists { + // nothing yet + } else if let Page::PeopleList(plist) = app.page { + list::enter_page(app, plist); + } else if matches!(app.page, Page::Person(_)) { + // nothing yet + } +} + pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { if app.page == Page::PeopleLists { lists::update(app, ctx, _frame, ui); From 7fd1c31aad0e61ab53a964363b54036d94deb467 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 7 Dec 2023 10:29:21 -0600 Subject: [PATCH 3/8] People List: Say "REMOTE: not found on Active Relays" when no remote data is available --- gossip-bin/src/ui/people/list.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 3e54c0ba..7162cb40 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -354,7 +354,7 @@ fn refresh_list_data(app: &mut GossipUi, list: gossip_lib::PersonList1) { .map(|v| v.value().clone()) .unwrap_or_default(); - let mut asof = "unknown".to_owned(); + let mut asof = "time unknown".to_owned(); if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(latest_event_data.when.0) { if let Ok(formatted) = stamp.format(time::macros::format_description!( "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" @@ -363,7 +363,9 @@ fn refresh_list_data(app: &mut GossipUi, list: gossip_lib::PersonList1) { } } - app.people_list.cache_remote_tag = if let Some(private_len) = latest_event_data.private_len { + app.people_list.cache_remote_tag = if latest_event_data.when.0 == 0 { + "REMOTE: not found on Active Relays".to_owned() + } else if let Some(private_len) = latest_event_data.private_len { format!( "REMOTE: {} (public_len={} private_len={})", asof, latest_event_data.public_len, private_len @@ -384,12 +386,14 @@ fn refresh_list_data(app: &mut GossipUi, list: gossip_lib::PersonList1) { } }; - let mut ledit = "unknown".to_owned(); - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_list_edit) { - if let Ok(formatted) = stamp.format(time::macros::format_description!( - "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" - )) { - ledit = formatted; + let mut ledit = "time unknown".to_owned(); + if last_list_edit > 0 { + if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_list_edit) { + if let Ok(formatted) = stamp.format(time::macros::format_description!( + "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" + )) { + ledit = formatted; + } } } From 2f4db68750e4a64ee048e798d78e91c36a8e2166 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 7 Dec 2023 13:31:30 -0600 Subject: [PATCH 4/8] rename widgets::search_filter_field() to widgets::search_field() --- gossip-bin/src/ui/relays/active.rs | 2 +- gossip-bin/src/ui/relays/known.rs | 2 +- gossip-bin/src/ui/relays/mine.rs | 2 +- gossip-bin/src/ui/widgets/mod.rs | 18 +++++++++--------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/gossip-bin/src/ui/relays/active.rs b/gossip-bin/src/ui/relays/active.rs index 6f89f73b..4a332d20 100644 --- a/gossip-bin/src/ui/relays/active.rs +++ b/gossip-bin/src/ui/relays/active.rs @@ -20,7 +20,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra btn_h_space!(ui); super::relay_sort_combo(app, ui); btn_h_space!(ui); - widgets::search_filter_field(ui, &mut app.relays.search, 200.0); + widgets::search_field(ui, &mut app.relays.search, 200.0); ui.add_space(200.0); // search_field somehow doesn't "take up" space if ui .button(RichText::new(Page::RelaysCoverage.name())) diff --git a/gossip-bin/src/ui/relays/known.rs b/gossip-bin/src/ui/relays/known.rs index 9d69a5d6..06ada4b5 100644 --- a/gossip-bin/src/ui/relays/known.rs +++ b/gossip-bin/src/ui/relays/known.rs @@ -16,7 +16,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr btn_h_space!(ui); super::relay_sort_combo(app, ui); btn_h_space!(ui); - widgets::search_filter_field(ui, &mut app.relays.search, 200.0); + widgets::search_field(ui, &mut app.relays.search, 200.0); }); // TBD time how long this takes. We don't want expensive code in the UI diff --git a/gossip-bin/src/ui/relays/mine.rs b/gossip-bin/src/ui/relays/mine.rs index fa412e68..569f8116 100644 --- a/gossip-bin/src/ui/relays/mine.rs +++ b/gossip-bin/src/ui/relays/mine.rs @@ -17,7 +17,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr btn_h_space!(ui); super::relay_sort_combo(app, ui); btn_h_space!(ui); - widgets::search_filter_field(ui, &mut app.relays.search, 200.0); + widgets::search_field(ui, &mut app.relays.search, 200.0); ui.add_space(200.0); // search_field somehow doesn't "take up" space widgets::set_important_button_visuals(ui, app); if ui.button("Advertise Relay List") diff --git a/gossip-bin/src/ui/widgets/mod.rs b/gossip-bin/src/ui/widgets/mod.rs index d305507c..eb7fa05a 100644 --- a/gossip-bin/src/ui/widgets/mod.rs +++ b/gossip-bin/src/ui/widgets/mod.rs @@ -90,16 +90,16 @@ pub fn break_anywhere_hyperlink_to(ui: &mut Ui, text: impl Into, url ui.hyperlink_to(job.job, url); } -pub fn search_filter_field(ui: &mut Ui, field: &mut String, width: f32) -> Response { +pub fn search_field(ui: &mut Ui, field: &mut String, width: f32) -> TextEditOutput { // search field - let response = ui.add( - TextEdit::singleline(field) - .text_color(ui.visuals().widgets.inactive.fg_stroke.color) - .desired_width(width), - ); + let output = TextEdit::singleline(field) + .text_color(ui.visuals().widgets.inactive.fg_stroke.color) + .desired_width(width) + .show(ui); + let rect = Rect::from_min_size( - response.rect.right_top() - vec2(response.rect.height(), 0.0), - vec2(response.rect.height(), response.rect.height()), + output.response.rect.right_top() - vec2(output.response.rect.height(), 0.0), + vec2(output.response.rect.height(), output.response.rect.height()), ); // search clear button @@ -117,7 +117,7 @@ pub fn search_filter_field(ui: &mut Ui, field: &mut String, width: f32) -> Respo field.clear(); } - response + output } pub(super) fn set_important_button_visuals(ui: &mut Ui, app: &GossipUi) { From 9986b08fc89747c5b7874e4153e5260104c65a3b Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 7 Dec 2023 13:32:53 -0600 Subject: [PATCH 5/8] Extract tagging search into a more general `widgets::show_contact_search()` --- gossip-bin/src/ui/feed/post.rs | 248 ++++---------------- gossip-bin/src/ui/widgets/contact_search.rs | 170 ++++++++++++++ gossip-bin/src/ui/widgets/mod.rs | 8 +- 3 files changed, 227 insertions(+), 199 deletions(-) create mode 100644 gossip-bin/src/ui/widgets/contact_search.rs diff --git a/gossip-bin/src/ui/feed/post.rs b/gossip-bin/src/ui/feed/post.rs index 0e86a87f..c16111b4 100644 --- a/gossip-bin/src/ui/feed/post.rs +++ b/gossip-bin/src/ui/feed/post.rs @@ -9,9 +9,9 @@ use egui_winit::egui::text::CCursor; use egui_winit::egui::text_edit::{CCursorRange, TextEditOutput}; use egui_winit::egui::Id; use gossip_lib::comms::ToOverlordMessage; +use gossip_lib::DmChannel; use gossip_lib::Relay; use gossip_lib::GLOBALS; -use gossip_lib::{DmChannel, Person}; use memoize::memoize; use nostr_types::{ContentSegment, NostrBech32, NostrUrl, ShatteredContent, Tag}; use std::collections::HashMap; @@ -396,38 +396,11 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram let enter_key; (app.draft_data.tagging_search_selected, enter_key) = if app.draft_data.tagging_search_substring.is_some() { - ui.input_mut(|i| { - // enter - let enter = i.count_and_consume_key(Modifiers::NONE, Key::Enter) > 0; - - // up / down - let mut index = app.draft_data.tagging_search_selected.unwrap_or(0); - let down = i.count_and_consume_key(Modifiers::NONE, Key::ArrowDown); - let up = i.count_and_consume_key(Modifiers::NONE, Key::ArrowUp); - index += down; - index = index.min( - app.draft_data - .tagging_search_results - .len() - .saturating_sub(1), - ); - index = index.saturating_sub(up); - - // tab will cycle down and wrap - let tab = i.count_and_consume_key(Modifiers::NONE, Key::Tab); - index += tab; - if index - > app - .draft_data - .tagging_search_results - .len() - .saturating_sub(1) - { - index = 0; - } - - (Some(index), enter) - }) + widgets::capture_keyboard_for_search( + ui, + app.draft_data.tagging_search_results.len(), + app.draft_data.tagging_search_selected, + ) } else { (None, false) }; @@ -687,176 +660,57 @@ fn show_tagging_result( output: &mut TextEditOutput, enter_key: bool, ) { - let pos = if let Some(cursor) = output.cursor_range { - let rect = output.galley.pos_from_cursor(&cursor.primary); // position within textedit - output.text_draw_pos + rect.center_bottom().to_vec2() - } else { - let rect = output.galley.pos_from_cursor(&output.galley.end()); // position within textedit - output.text_draw_pos + rect.center_bottom().to_vec2() - }; + let mut selected = app.draft_data.tagging_search_selected; + widgets::show_contact_search( + ui, + app, + output, + &mut selected, + app.draft_data.tagging_search_results.clone(), + enter_key, + |ui, app, output, pair| { + // remove @ and search text + let search = if let Some(search) = app.draft_data.tagging_search_searched.as_ref() { + search.clone() + } else { + "".to_string() + }; - // always compute the tooltip, but it is only shown when - // is_open is true. This is so we get the animation. - let frame = egui::Frame::popup(ui.style()) - .rounding(egui::Rounding::ZERO) - .inner_margin(egui::Margin::same(0.0)); - let area = egui::Area::new(ui.auto_id_with("compose-tagging-tooltip")) - .fixed_pos(pos) - .movable(false) - .constrain(true) - .interactable(true) - .order(egui::Order::Middle); + // complete name and add replacement + let name = pair.0.clone(); + let nostr_url: NostrUrl = pair.1.into(); + app.draft_data.draft = app + .draft_data + .draft + .as_str() + .replace(&format!("@{}", search), name.as_str()) + .to_string(); - // show search results - if !app.draft_data.tagging_search_results.is_empty() { - area.show(ui.ctx(), |ui| { - frame.show(ui, |ui| { - egui::ScrollArea::vertical() - .max_width(widgets::TAGG_WIDTH) - .max_height(250.0) - .show(ui, |ui| { - // need to clone results to avoid immutable borrow error on app. - let pairs = app.draft_data.tagging_search_results.clone(); - for (i, pair) in pairs.iter().enumerate() { - let avatar = if let Some(avatar) = app.try_get_avatar(ui.ctx(), &pair.1) - { - avatar - } else { - app.placeholder_avatar.clone() - }; + // move cursor to end of replacement + if let Some(pos) = app.draft_data.draft.find(name.as_str()) { + let cpos = pos + name.len(); + let mut state = output.state.clone(); + let mut ccrange = CCursorRange::default(); + ccrange.primary.index = cpos; + ccrange.secondary.index = cpos; + state.set_ccursor_range(Some(ccrange)); + state.store(ui.ctx(), output.response.id); - let frame = egui::Frame::none() - .rounding(egui::Rounding::ZERO) - .inner_margin(egui::Margin::symmetric(10.0, 5.0)); - let mut prepared = frame.begin(ui); + // add it to our replacement list + app.draft_data + .replacements + .insert(name, ContentSegment::NostrUrl(nostr_url)); + app.draft_data.replacements_changed = true; - prepared.content_ui.set_min_width(widgets::TAGG_WIDTH); - prepared.content_ui.set_max_width(widgets::TAGG_WIDTH); - prepared.content_ui.set_min_height(27.0); + // clear tagging search + app.draft_data.tagging_search_substring = None; + } + }, + ); - let frame_rect = (prepared.frame.inner_margin - + prepared.frame.outer_margin) - .expand_rect(prepared.content_ui.min_rect()); + app.draft_data.tagging_search_selected = selected; - let response = ui - .interact( - frame_rect, - ui.auto_id_with(pair.1.as_hex_string()), - egui::Sense::click(), - ) - .on_hover_cursor(egui::CursorIcon::PointingHand); - - // mouse hover moves selected index - app.draft_data.tagging_search_selected = if response.hovered() { - Some(i) - } else { - app.draft_data.tagging_search_selected - }; - let is_selected = Some(i) == app.draft_data.tagging_search_selected; - - { - // render inside of frame using prepared.content_ui - let ui = &mut prepared.content_ui; - if is_selected { - app.theme.on_accent_style(ui.style_mut()) - } - let person = GLOBALS - .storage - .read_person(&pair.1) - .unwrap_or(Some(Person::new(pair.1))) - .unwrap_or(Person::new(pair.1)); - ui.horizontal(|ui| { - widgets::paint_avatar( - ui, - &person, - &avatar, - widgets::AvatarSize::Mini, - ); - ui.vertical(|ui| { - widgets::truncated_label( - ui, - RichText::new(&pair.0).small(), - widgets::TAGG_WIDTH - 33.0, - ); - - let mut nip05 = - RichText::new(person.nip05().unwrap_or_default()) - .weak() - .small(); - if !person.nip05_valid { - nip05 = nip05.strikethrough() - } - widgets::truncated_label( - ui, - nip05, - widgets::TAGG_WIDTH - 33.0, - ); - }); - }) - }; - - prepared.frame.fill = if is_selected { - app.theme.accent_color() - } else { - egui::Color32::TRANSPARENT - }; - - prepared.end(ui); - - if is_selected { - response.scroll_to_me(None) - } - let clicked = response.clicked(); - if clicked || (enter_key && is_selected) { - // remove @ and search text - let search = if let Some(search) = - app.draft_data.tagging_search_searched.as_ref() - { - search.clone() - } else { - "".to_string() - }; - - // complete name and add replacement - let name = pair.0.clone(); - let nostr_url: NostrUrl = pair.1.into(); - app.draft_data.draft = app - .draft_data - .draft - .as_str() - .replace(&format!("@{}", search), name.as_str()) - .to_string(); - - // move cursor to end of replacement - if let Some(pos) = app.draft_data.draft.find(name.as_str()) { - let cpos = pos + name.len(); - let mut state = output.state.clone(); - let mut ccrange = CCursorRange::default(); - ccrange.primary.index = cpos; - ccrange.secondary.index = cpos; - state.set_ccursor_range(Some(ccrange)); - state.store(ui.ctx(), output.response.id); - - // add it to our replacement list - app.draft_data - .replacements - .insert(name, ContentSegment::NostrUrl(nostr_url)); - app.draft_data.replacements_changed = true; - - // clear tagging search - app.draft_data.tagging_search_substring = None; - } - } - } - }); - }); - }); - } - - let is_open = app.draft_data.tagging_search_substring.is_some(); - area.show_open_close_animation(ui.ctx(), &frame, is_open); - - if !is_open { + if app.draft_data.tagging_search_substring.is_none() { // no more search substring, clear results app.draft_data.tagging_search_searched = None; app.draft_data.tagging_search_results.clear(); diff --git a/gossip-bin/src/ui/widgets/contact_search.rs b/gossip-bin/src/ui/widgets/contact_search.rs new file mode 100644 index 00000000..66a8714b --- /dev/null +++ b/gossip-bin/src/ui/widgets/contact_search.rs @@ -0,0 +1,170 @@ +use egui_winit::egui::{self, text_edit::TextEditOutput, Key, Modifiers, RichText, Ui}; +use gossip_lib::{Person, GLOBALS}; +use nostr_types::PublicKey; + +use crate::ui::GossipUi; + +pub(in crate::ui) fn show_contact_search( + ui: &mut Ui, + app: &mut GossipUi, + output: &mut TextEditOutput, + selected: &mut Option, + search_results: Vec<(String, PublicKey)>, + enter_key: bool, + on_select_callback: impl Fn(&mut Ui, &mut GossipUi, &mut TextEditOutput, &(String, PublicKey)), +) { + let pos = if let Some(cursor) = output.cursor_range { + let rect = output.galley.pos_from_cursor(&cursor.primary); // position within textedit + output.text_draw_pos + rect.center_bottom().to_vec2() + } else { + let rect = output.galley.pos_from_cursor(&output.galley.end()); // position within textedit + output.text_draw_pos + rect.center_bottom().to_vec2() + }; + + // always compute the tooltip, but it is only shown when + // is_open is true. This is so we get the animation. + let frame = egui::Frame::popup(ui.style()) + .rounding(egui::Rounding::ZERO) + .inner_margin(egui::Margin::same(0.0)); + let area = egui::Area::new(ui.auto_id_with("compose-tagging-tooltip")) + .fixed_pos(pos) + .movable(false) + .constrain(true) + .interactable(true) + .order(egui::Order::Foreground); + + let is_open = !search_results.is_empty(); + + // show search results + if is_open { + area.show(ui.ctx(), |ui| { + frame.show(ui, |ui| { + egui::ScrollArea::vertical() + .max_width(super::TAGG_WIDTH) + .max_height(250.0) + .show(ui, |ui| { + for (i, pair) in search_results.iter().enumerate() { + let avatar = if let Some(avatar) = app.try_get_avatar(ui.ctx(), &pair.1) + { + avatar + } else { + app.placeholder_avatar.clone() + }; + + let frame = egui::Frame::none() + .rounding(egui::Rounding::ZERO) + .inner_margin(egui::Margin::symmetric(10.0, 5.0)); + let mut prepared = frame.begin(ui); + + prepared.content_ui.set_min_width(super::TAGG_WIDTH); + prepared.content_ui.set_max_width(super::TAGG_WIDTH); + prepared.content_ui.set_min_height(27.0); + + let frame_rect = (prepared.frame.inner_margin + + prepared.frame.outer_margin) + .expand_rect(prepared.content_ui.min_rect()); + + let response = ui + .interact( + frame_rect, + ui.auto_id_with(pair.1.as_hex_string()), + egui::Sense::click(), + ) + .on_hover_cursor(egui::CursorIcon::PointingHand); + + // mouse hover moves selected index + *selected = if response.hovered() { + Some(i) + } else { + *selected + }; + let is_selected = Some(i) == *selected; + + { + // render inside of frame using prepared.content_ui + let ui = &mut prepared.content_ui; + if is_selected { + app.theme.on_accent_style(ui.style_mut()) + } + let person = GLOBALS + .storage + .read_person(&pair.1) + .unwrap_or(Some(Person::new(pair.1))) + .unwrap_or(Person::new(pair.1)); + ui.horizontal(|ui| { + super::paint_avatar( + ui, + &person, + &avatar, + super::AvatarSize::Mini, + ); + ui.vertical(|ui| { + super::truncated_label( + ui, + RichText::new(&pair.0).small(), + super::TAGG_WIDTH - 33.0, + ); + + let mut nip05 = + RichText::new(person.nip05().unwrap_or_default()) + .weak() + .small(); + if !person.nip05_valid { + nip05 = nip05.strikethrough() + } + super::truncated_label(ui, nip05, super::TAGG_WIDTH - 33.0); + }); + }) + }; + + prepared.frame.fill = if is_selected { + app.theme.accent_color() + } else { + egui::Color32::TRANSPARENT + }; + + prepared.end(ui); + + if is_selected { + response.scroll_to_me(None) + } + let clicked = response.interact(egui::Sense::click()).clicked(); + if clicked || (enter_key && is_selected) { + on_select_callback(ui, app, output, pair); + } + } + }); + }); + }); + } + + area.show_open_close_animation(ui.ctx(), &frame, is_open); +} + +pub(in crate::ui) fn capture_keyboard_for_search( + ui: &mut Ui, + result_len: usize, + selected: Option, +) -> (Option, bool) { + ui.input_mut(|i| { + // enter + let enter = i.count_and_consume_key(Modifiers::NONE, Key::Enter) > 0; + + // up / down + let mut index = selected.unwrap_or(0); + let down = i.count_and_consume_key(Modifiers::NONE, Key::ArrowDown); + let up = i.count_and_consume_key(Modifiers::NONE, Key::ArrowUp); + index += down; + index = index.min(result_len.saturating_sub(1)); + index = index.saturating_sub(up); + + // tab will cycle down and wrap + let tab = i.count_and_consume_key(Modifiers::NONE, Key::Tab); + index += tab; + if index > result_len.saturating_sub(1) { + index = 0; + } + + (Some(index), enter) + }) +} diff --git a/gossip-bin/src/ui/widgets/mod.rs b/gossip-bin/src/ui/widgets/mod.rs index eb7fa05a..221be753 100644 --- a/gossip-bin/src/ui/widgets/mod.rs +++ b/gossip-bin/src/ui/widgets/mod.rs @@ -1,14 +1,15 @@ mod avatar; pub(crate) use avatar::{paint_avatar, AvatarSize}; -mod more_menu; -pub(super) use more_menu::MoreMenu; +mod contact_search; +pub(super) use contact_search::{capture_keyboard_for_search, show_contact_search}; mod copy_button; pub(crate) mod list_entry; pub use copy_button::{CopyButton, COPY_SYMBOL_SIZE}; mod nav_item; +use egui_winit::egui::text_edit::TextEditOutput; use egui_winit::egui::{ self, vec2, FontSelection, Rect, Response, Sense, TextEdit, Ui, WidgetText, }; @@ -20,6 +21,9 @@ pub use relay_entry::{RelayEntry, RelayEntryView}; mod modal_popup; pub use modal_popup::modal_popup; +mod more_menu; +pub(super) use more_menu::MoreMenu; + mod information_popup; pub use information_popup::InformationPopup; pub use information_popup::ProfilePopup; From d567d23fb0b311c0c2213289f928fca87869155b Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 7 Dec 2023 13:33:46 -0600 Subject: [PATCH 6/8] People List: Add contact search functionality to "Add contact" dialogue --- gossip-bin/src/ui/mod.rs | 4 +- gossip-bin/src/ui/people/list.rs | 395 ++++++++++++++-------- gossip-bin/src/ui/widgets/mod.rs | 2 +- gossip-bin/src/ui/wizard/follow_people.rs | 14 +- 4 files changed, 271 insertions(+), 144 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index c81f61ec..56dd2c85 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -410,7 +410,7 @@ struct GossipUi { delegatee_tag_str: String, // User entry: general - follow_someone: String, + add_contact: String, add_relay: String, // dep password: String, password2: String, @@ -652,7 +652,7 @@ impl GossipUi { editing_metadata: false, metadata: Metadata::new(), delegatee_tag_str: "".to_owned(), - follow_someone: "".to_owned(), + add_contact: "".to_owned(), add_relay: "".to_owned(), password: "".to_owned(), password2: "".to_owned(), diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 7162cb40..e0b88dbb 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -1,9 +1,10 @@ -use std::time::{Instant, Duration}; +use std::time::{Duration, Instant}; use super::{GossipUi, Page}; use crate::ui::widgets; use eframe::egui; use egui::{Context, RichText, Ui, Vec2}; +use egui_winit::egui::text_edit::TextEditOutput; use egui_winit::egui::vec2; use gossip_lib::comms::ToOverlordMessage; use gossip_lib::{Person, PersonList, GLOBALS}; @@ -17,6 +18,12 @@ pub(crate) struct ListUi { cache_remote_tag: String, cache_local_tag: String, + // add contact + add_contact_search: String, + add_contact_searched: Option, + add_contact_search_results: Vec<(String, PublicKey)>, + add_contact_search_selected: Option, + configure_list_menu_active: bool, entering_follow_someone_on_list: bool, clear_list_needs_confirm: bool, @@ -31,6 +38,13 @@ impl ListUi { cache_people: Vec::new(), cache_remote_tag: String::new(), cache_local_tag: String::new(), + + // add contact + add_contact_search: String::new(), + add_contact_searched: None, + add_contact_search_results: Vec::new(), + add_contact_search_selected: None, + configure_list_menu_active: false, entering_follow_someone_on_list: false, clear_list_needs_confirm: false, @@ -49,37 +63,58 @@ pub(super) fn update( ui: &mut Ui, list: PersonList, ) { - if app.people_list.cache_next_refresh < Instant::now() || - app.people_list.cache_last_list.is_none() || - app.people_list.cache_last_list.unwrap() != list { + if app.people_list.cache_next_refresh < Instant::now() + || app.people_list.cache_last_list.is_none() + || app.people_list.cache_last_list.unwrap() != list + { refresh_list_data(app, list); } + // process popups first + if app.people_list.clear_list_needs_confirm { + render_clear_list_confirm_popup(ui, app, list); + } + if app.people_list.entering_follow_someone_on_list { + render_add_contact_popup(ui, app, list); + } + + // disable rest of ui when popups are open + let enabled = !app.people_list.entering_follow_someone_on_list + && !app.people_list.clear_list_needs_confirm; + // render page - widgets::page_header(ui, format!("{} ({})", list.name(), app.people_list.cache_people.len()), |ui| { - ui.add_enabled_ui(true, |ui| { - let min_size = vec2(50.0, 20.0); + widgets::page_header( + ui, + format!("{} ({})", list.name(), app.people_list.cache_people.len()), + |ui| { + ui.add_enabled_ui(enabled, |ui| { + let min_size = vec2(50.0, 20.0); - widgets::MoreMenu::new(&app) - .with_min_size(min_size) - .show(ui, &mut app.people_list.configure_list_menu_active, |ui|{ - // since we are displaying over an accent color background, load that style - app.theme.accent_button_2_style(ui.style_mut()); + widgets::MoreMenu::new(&app).with_min_size(min_size).show( + ui, + &mut app.people_list.configure_list_menu_active, + |ui| { + // since we are displaying over an accent color background, load that style + app.theme.accent_button_2_style(ui.style_mut()); - if ui.button("Clear All").clicked() { - app.people_list.clear_list_needs_confirm = true; - } + if ui.button("Clear All").clicked() { + app.people_list.clear_list_needs_confirm = true; + } - // ui.add_space(8.0); + // ui.add_space(8.0); + }, + ); }); - }); - btn_h_space!(ui); + btn_h_space!(ui); - if ui.button("Add contact").clicked() { - app.people_list.entering_follow_someone_on_list = true; - } - }); + if ui.button("Add contact").clicked() { + app.people_list.entering_follow_someone_on_list = true; + } + }, + ); + + ui.set_enabled(enabled); if GLOBALS.signer.is_ready() { ui.vertical(|ui| { @@ -146,152 +181,172 @@ pub(super) fn update( }); } - if app.people_list.clear_list_needs_confirm { - const DLG_SIZE: Vec2 = vec2(250.0, 40.0); - if widgets::modal_popup(ui, DLG_SIZE, |ui| { - ui.vertical(|ui| { - ui.label("Are you sure you want to clear this list?"); - ui.add_space(10.0); - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT),|ui| { - ui.horizontal(|ui| { - if ui.button("Cancel").clicked() { - app.people_list.clear_list_needs_confirm = false; - } - ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui|{ - if ui.button("YES, CLEAR ALL").clicked() { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::ClearPersonList(list)); - app.people_list.clear_list_needs_confirm = false; - } - }); - }); - }); - }); - }).inner.clicked() { - app.people_list.clear_list_needs_confirm = false; - } - } - ui.add_space(10.0); app.vert_scroll_area().show(ui, |ui| { // not nice but needed because of 'app' borrow in closure let people = app.people_list.cache_people.clone(); for (person, public) in people.iter() { - let row_response = widgets::list_entry::make_frame(ui) - .show(ui, |ui| { - ui.horizontal(|ui| { - // Avatar first - let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) { - avatar - } else { - app.placeholder_avatar.clone() - }; - let avatar_height = widgets::paint_avatar(ui, person, &avatar, widgets::AvatarSize::Feed).rect.height(); + let row_response = widgets::list_entry::make_frame(ui).show(ui, |ui| { + ui.horizontal(|ui| { + // Avatar first + let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) { + avatar + } else { + app.placeholder_avatar.clone() + }; + let avatar_height = + widgets::paint_avatar(ui, person, &avatar, widgets::AvatarSize::Feed) + .rect + .height(); - ui.add_space(20.0); + ui.add_space(20.0); - ui.vertical(|ui| { - ui.set_min_height(avatar_height); + ui.vertical(|ui| { + ui.set_min_height(avatar_height); + ui.horizontal(|ui| { + ui.label(GossipUi::person_name(person)); + + ui.add_space(10.0); + + if !GLOBALS + .storage + .have_persons_relays(person.pubkey) + .unwrap_or(false) + { + ui.label( + RichText::new("Relay list not found") + .color(app.theme.warning_marker_text_color()), + ); + } + }); + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { ui.horizontal(|ui| { - ui.label(GossipUi::person_name(person)); + ui.label( + RichText::new(gossip_lib::names::pubkey_short(&person.pubkey)) + .weak(), + ); ui.add_space(10.0); - if !GLOBALS - .storage - .have_persons_relays(person.pubkey) - .unwrap_or(false) - { - ui.label( - RichText::new("Relay list not found") - .color(app.theme.warning_marker_text_color()), - ); - } + ui.label(GossipUi::richtext_from_person_nip05(person)); }); - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { - ui.horizontal(|ui| { - ui.label(RichText::new(gossip_lib::names::pubkey_short(&person.pubkey)).weak()); - - ui.add_space(10.0); - - ui.label(GossipUi::richtext_from_person_nip05(person)); - }); - }); - }); - - ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { - ui.set_min_height(avatar_height); - // actions - if ui.link("Remove").clicked() { - let _ = GLOBALS - .storage - .remove_person_from_list(&person.pubkey, list, None); - } - - ui.add_space(20.0); - - // private / public switch - if crate::ui::components::switch_simple(ui, *public).clicked() { - let _ = GLOBALS.storage.add_person_to_list( - &person.pubkey, - list, - !*public, - None, - ); - mark_refresh(app); - } - ui.label(if *public { "public" } else { "private" }); }); }); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + ui.set_min_height(avatar_height); + // actions + if ui.link("Remove").clicked() { + let _ = + GLOBALS + .storage + .remove_person_from_list(&person.pubkey, list, None); + } + + ui.add_space(20.0); + + // private / public switch + if crate::ui::components::switch_simple(ui, *public).clicked() { + let _ = GLOBALS.storage.add_person_to_list( + &person.pubkey, + list, + !*public, + None, + ); + mark_refresh(app); + } + ui.label(if *public { "public" } else { "private" }); + }); + }); }); if row_response .response .interact(egui::Sense::click()) .on_hover_cursor(egui::CursorIcon::PointingHand) - .clicked() { - app.set_page(ctx, Page::Person(person.pubkey)); + .clicked() + { + app.set_page(ctx, Page::Person(person.pubkey)); } } }); +} - if app.people_list.entering_follow_someone_on_list { - const DLG_SIZE: Vec2 = vec2(400.0, 200.0); - let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, |ui| { - // TODO use tagging search here +fn render_add_contact_popup(ui: &mut Ui, app: &mut GossipUi, list: gossip_lib::PersonList1) { + const DLG_SIZE: Vec2 = vec2(400.0, 240.0); + let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, |ui| { + let enter_key; + (app.people_list.add_contact_search_selected, enter_key) = + if app.people_list.add_contact_search_results.is_empty() { + (None, false) + } else { + widgets::capture_keyboard_for_search( + ui, + app.people_list.add_contact_search_results.len(), + app.people_list.add_contact_search_selected, + ) + }; - ui.heading("Follow someone"); + ui.heading("Add contact to the list"); + ui.add_space(8.0); - ui.add_space(8.0); + ui.label("Search for known contacts to add"); + ui.add_space(8.0); + let mut output = + widgets::search_field(ui, &mut app.people_list.add_contact_search, f32::INFINITY); + + let mut selected = app.people_list.add_contact_search_selected; + widgets::show_contact_search( + ui, + app, + &mut output, + &mut selected, + app.people_list.add_contact_search_results.clone(), + enter_key, + |_, app, _, pair| { + app.people_list.add_contact_search = pair.0.clone(); + app.people_list.add_contact_search_results.clear(); + app.people_list.add_contact_search_selected = None; + app.add_contact = pair.1.as_bech32_string(); + }, + ); + app.people_list.add_contact_search_selected = selected; + + recalc_add_contact_search(app, &mut output); + + ui.add_space(8.0); + + ui.label("To add a new contact to this list enter their npub, hex key, nprofle or nip-05 address"); + ui.add_space(8.0); + + ui.add( + text_edit_multiline!(app, app.add_contact) + .desired_width(f32::INFINITY) + .hint_text("npub1, hex key, nprofile1, or user@domain"), + ); + + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { ui.horizontal(|ui| { - ui.label("Enter"); - ui.add( - text_edit_line!(app, app.follow_someone) - .hint_text("npub1, hex key, nprofile1, or user@domain"), - ); - }); - - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { - ui.horizontal(|ui| { - if ui.button("follow").clicked() { + ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + app.theme.accent_button_1_style(ui.style_mut()); + if ui.button("Add Contact").clicked() { if let Ok(pubkey) = - PublicKey::try_from_bech32_string(app.follow_someone.trim(), true) + PublicKey::try_from_bech32_string(app.add_contact.trim(), true) { let _ = GLOBALS .to_overlord .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); app.people_list.entering_follow_someone_on_list = false; } else if let Ok(pubkey) = - PublicKey::try_from_hex_string(app.follow_someone.trim(), true) + PublicKey::try_from_hex_string(app.add_contact.trim(), true) { let _ = GLOBALS .to_overlord .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); app.people_list.entering_follow_someone_on_list = false; } else if let Ok(profile) = - Profile::try_from_bech32_string(app.follow_someone.trim(), true) + Profile::try_from_bech32_string(app.add_contact.trim(), true) { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNprofile( profile.clone(), @@ -299,9 +354,9 @@ pub(super) fn update( true, )); app.people_list.entering_follow_someone_on_list = false; - } else if gossip_lib::nip05::parse_nip05(app.follow_someone.trim()).is_ok() { + } else if gossip_lib::nip05::parse_nip05(app.add_contact.trim()).is_ok() { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNip05( - app.follow_someone.trim().to_owned(), + app.add_contact.trim().to_owned(), list, true, )); @@ -311,15 +366,83 @@ pub(super) fn update( .write() .write("Invalid pubkey.".to_string()); } - app.follow_someone = "".to_owned(); + app.add_contact = "".to_owned(); + app.people_list.add_contact_search.clear(); + app.people_list.add_contact_searched = None; + app.people_list.add_contact_search_selected = None; + app.people_list.add_contact_search_results.clear(); } }); }); - }); - if ret.inner.clicked() { - app.people_list.entering_follow_someone_on_list = false; + }); + if ret.inner.clicked() { + app.people_list.entering_follow_someone_on_list = false; + app.people_list.add_contact_search.clear(); + app.people_list.add_contact_searched = None; + app.people_list.add_contact_search_selected = None; + app.people_list.add_contact_search_results.clear(); + } +} + +fn recalc_add_contact_search(app: &mut GossipUi, output: &mut TextEditOutput) { + // only recalc if search text changed + if app.people_list.add_contact_search.len() > 2 && output.cursor_range.is_some() { + if Some(&app.people_list.add_contact_search) + != app.people_list.add_contact_searched.as_ref() + { + let mut pairs = GLOBALS + .people + .search_people_to_tag(app.people_list.add_contact_search.as_str()) + .unwrap_or_default(); + // followed contacts first + pairs.sort_by(|(_, ak), (_, bk)| { + let af = GLOBALS + .storage + .is_person_in_list(ak, gossip_lib::PersonList::Followed) + .unwrap_or(false); + let bf = GLOBALS + .storage + .is_person_in_list(bk, gossip_lib::PersonList::Followed) + .unwrap_or(false); + bf.cmp(&af).then(std::cmp::Ordering::Greater) + }); + app.people_list.add_contact_searched = Some(app.people_list.add_contact_search.clone()); + app.people_list.add_contact_search_results = pairs.to_owned(); } + } else { + app.people_list.add_contact_searched = None; + app.people_list.add_contact_search_results.clear(); + } +} + +fn render_clear_list_confirm_popup(ui: &mut Ui, app: &mut GossipUi, list: PersonList) { + const DLG_SIZE: Vec2 = vec2(250.0, 40.0); + if widgets::modal_popup(ui, DLG_SIZE, |ui| { + ui.vertical(|ui| { + ui.label("Are you sure you want to clear this list?"); + ui.add_space(10.0); + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { + ui.horizontal(|ui| { + if ui.button("Cancel").clicked() { + app.people_list.clear_list_needs_confirm = false; + } + ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui| { + if ui.button("YES, CLEAR ALL").clicked() { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::ClearPersonList(list)); + app.people_list.clear_list_needs_confirm = false; + } + }); + }); + }); + }); + }) + .inner + .clicked() + { + app.people_list.clear_list_needs_confirm = false; } } @@ -397,7 +520,11 @@ fn refresh_list_data(app: &mut GossipUi, list: gossip_lib::PersonList1) { } } - app.people_list.cache_local_tag = format!("LOCAL: {} (size={})", ledit, app.people_list.cache_people.len()); + app.people_list.cache_local_tag = format!( + "LOCAL: {} (size={})", + ledit, + app.people_list.cache_people.len() + ); app.people_list.cache_next_refresh = Instant::now() + Duration::new(1, 0); app.people_list.cache_last_list = Some(list); diff --git a/gossip-bin/src/ui/widgets/mod.rs b/gossip-bin/src/ui/widgets/mod.rs index 221be753..766328af 100644 --- a/gossip-bin/src/ui/widgets/mod.rs +++ b/gossip-bin/src/ui/widgets/mod.rs @@ -11,7 +11,7 @@ pub use copy_button::{CopyButton, COPY_SYMBOL_SIZE}; mod nav_item; use egui_winit::egui::text_edit::TextEditOutput; use egui_winit::egui::{ - self, vec2, FontSelection, Rect, Response, Sense, TextEdit, Ui, WidgetText, + self, vec2, FontSelection, Rect, Sense, TextEdit, Ui, WidgetText, }; pub use nav_item::NavItem; diff --git a/gossip-bin/src/ui/wizard/follow_people.rs b/gossip-bin/src/ui/wizard/follow_people.rs index eb2acb59..114a891e 100644 --- a/gossip-bin/src/ui/wizard/follow_people.rs +++ b/gossip-bin/src/ui/wizard/follow_people.rs @@ -86,7 +86,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr ui.horizontal(|ui| { ui.label("Follow Someone:"); if ui - .add(text_edit_line!(app, app.follow_someone).hint_text( + .add(text_edit_line!(app, app.add_contact).hint_text( "Enter a key (bech32 npub1 or hex), or an nprofile, or a DNS id (user@domain)", )) .changed() @@ -94,14 +94,14 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr app.wizard_state.error = None; } if ui.button("follow").clicked() { - if let Ok(pubkey) = PublicKey::try_from_bech32_string(app.follow_someone.trim(), true) { + if let Ok(pubkey) = PublicKey::try_from_bech32_string(app.add_contact.trim(), true) { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowPubkey( pubkey, PersonList::Followed, true, )); } else if let Ok(pubkey) = - PublicKey::try_from_hex_string(app.follow_someone.trim(), true) + PublicKey::try_from_hex_string(app.add_contact.trim(), true) { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowPubkey( pubkey, @@ -109,23 +109,23 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr true, )); } else if let Ok(profile) = - Profile::try_from_bech32_string(app.follow_someone.trim(), true) + Profile::try_from_bech32_string(app.add_contact.trim(), true) { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNprofile( profile, PersonList::Followed, true, )); - } else if gossip_lib::nip05::parse_nip05(app.follow_someone.trim()).is_ok() { + } else if gossip_lib::nip05::parse_nip05(app.add_contact.trim()).is_ok() { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNip05( - app.follow_someone.trim().to_owned(), + app.add_contact.trim().to_owned(), PersonList::Followed, true, )); } else { app.wizard_state.error = Some("ERROR: Invalid pubkey".to_owned()); } - app.follow_someone = "".to_owned(); + app.add_contact = "".to_owned(); } }); From 8a9645b664596361bf65e30e1c6a66b812228f0f Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 7 Dec 2023 14:54:45 -0600 Subject: [PATCH 7/8] People Lists: Style clear list confirmation --- gossip-bin/src/ui/people/list.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index e0b88dbb..9ffd8dd7 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -424,15 +424,18 @@ fn render_clear_list_confirm_popup(ui: &mut Ui, app: &mut GossipUi, list: Person ui.add_space(10.0); ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { ui.horizontal(|ui| { + app.theme.accent_button_2_style(ui.style_mut()); if ui.button("Cancel").clicked() { app.people_list.clear_list_needs_confirm = false; } ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui| { + app.theme.accent_button_1_style(ui.style_mut()); if ui.button("YES, CLEAR ALL").clicked() { let _ = GLOBALS .to_overlord .send(ToOverlordMessage::ClearPersonList(list)); app.people_list.clear_list_needs_confirm = false; + mark_refresh(app); } }); }); From 31f824c6a350931b7a3cd23027fe770840ed9855 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 7 Dec 2023 15:35:19 -0600 Subject: [PATCH 8/8] People List: Add "Add and continue" button to quickly keep adding people to a list --- gossip-bin/src/ui/people/list.rs | 44 ++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 9ffd8dd7..e080c398 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -317,7 +317,7 @@ fn render_add_contact_popup(ui: &mut Ui, app: &mut GossipUi, list: gossip_lib::P ui.add_space(8.0); - ui.label("To add a new contact to this list enter their npub, hex key, nprofle or nip-05 address"); + ui.label("To add a new contact to this list enter their npub, hex key, nprofile or nip-05 address"); ui.add_space(8.0); ui.add( @@ -329,22 +329,39 @@ fn render_add_contact_popup(ui: &mut Ui, app: &mut GossipUi, list: gossip_lib::P ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { ui.horizontal(|ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + let mut try_add = false; + let mut want_close = false; + let mut can_close = false; + app.theme.accent_button_1_style(ui.style_mut()); - if ui.button("Add Contact").clicked() { + if ui.button("Add and close").clicked() { + try_add |= true; + want_close = true; + } + + btn_h_space!(ui); + + app.theme.accent_button_2_style(ui.style_mut()); + if ui.button("Add and continue").clicked() { + try_add |= true; + } + + if try_add { + let mut add_failed = false; if let Ok(pubkey) = PublicKey::try_from_bech32_string(app.add_contact.trim(), true) { let _ = GLOBALS .to_overlord .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); - app.people_list.entering_follow_someone_on_list = false; + can_close = true; } else if let Ok(pubkey) = PublicKey::try_from_hex_string(app.add_contact.trim(), true) { let _ = GLOBALS .to_overlord .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); - app.people_list.entering_follow_someone_on_list = false; + can_close = true; } else if let Ok(profile) = Profile::try_from_bech32_string(app.add_contact.trim(), true) { @@ -353,7 +370,7 @@ fn render_add_contact_popup(ui: &mut Ui, app: &mut GossipUi, list: gossip_lib::P list, true, )); - app.people_list.entering_follow_someone_on_list = false; + can_close = true; } else if gossip_lib::nip05::parse_nip05(app.add_contact.trim()).is_ok() { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNip05( app.add_contact.trim().to_owned(), @@ -361,16 +378,23 @@ fn render_add_contact_popup(ui: &mut Ui, app: &mut GossipUi, list: gossip_lib::P true, )); } else { + add_failed = true; GLOBALS .status_queue .write() .write("Invalid pubkey.".to_string()); } - app.add_contact = "".to_owned(); - app.people_list.add_contact_search.clear(); - app.people_list.add_contact_searched = None; - app.people_list.add_contact_search_selected = None; - app.people_list.add_contact_search_results.clear(); + if !add_failed { + app.add_contact = "".to_owned(); + app.people_list.add_contact_search.clear(); + app.people_list.add_contact_searched = None; + app.people_list.add_contact_search_selected = None; + app.people_list.add_contact_search_results.clear(); + } + if want_close && can_close { + app.people_list.entering_follow_someone_on_list = false; + mark_refresh(app); + } } }); });