mirror of
https://github.com/mikedilger/gossip.git
synced 2024-09-29 08:21:47 +00:00
Merge remote-tracking branch 'bushmann/feature/people-list-ui' into unstable
This commit is contained in:
commit
0b855bf0af
@ -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)
|
||||
};
|
||||
@ -565,7 +538,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")
|
||||
@ -687,131 +660,17 @@ 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()
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
// 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()
|
||||
};
|
||||
|
||||
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(widgets::TAGG_WIDTH);
|
||||
prepared.content_ui.set_max_width(widgets::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
|
||||
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(
|
||||
let mut selected = app.draft_data.tagging_search_selected;
|
||||
widgets::show_contact_search(
|
||||
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) {
|
||||
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()
|
||||
{
|
||||
let search = if let Some(search) = app.draft_data.tagging_search_searched.as_ref() {
|
||||
search.clone()
|
||||
} else {
|
||||
"".to_string()
|
||||
@ -846,17 +705,12 @@ fn show_tagging_result(
|
||||
// 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);
|
||||
app.draft_data.tagging_search_selected = selected;
|
||||
|
||||
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();
|
||||
|
@ -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<Id>,
|
||||
render_qr: Option<Id>,
|
||||
@ -401,10 +410,8 @@ struct GossipUi {
|
||||
delegatee_tag_str: String,
|
||||
|
||||
// User entry: general
|
||||
entering_follow_someone_on_list: bool,
|
||||
follow_someone: String,
|
||||
add_contact: 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_contact: "".to_owned(),
|
||||
add_relay: "".to_owned(),
|
||||
clear_list_needs_confirm: false,
|
||||
password: "".to_owned(),
|
||||
password2: "".to_owned(),
|
||||
password3: "".to_owned(),
|
||||
@ -729,6 +735,7 @@ impl GossipUi {
|
||||
self.close_all_menus(ctx);
|
||||
}
|
||||
Page::PeopleLists => {
|
||||
people::enter_page(self);
|
||||
self.close_all_menus(ctx);
|
||||
}
|
||||
Page::Person(pubkey) => {
|
||||
@ -1331,6 +1338,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,
|
||||
|
@ -1,12 +1,61 @@
|
||||
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};
|
||||
use nostr_types::{Profile, PublicKey};
|
||||
|
||||
pub(crate) struct ListUi {
|
||||
// cache
|
||||
cache_last_list: Option<PersonList>,
|
||||
cache_next_refresh: Instant,
|
||||
cache_people: Vec<(Person, bool)>,
|
||||
cache_remote_tag: String,
|
||||
cache_local_tag: String,
|
||||
|
||||
// add contact
|
||||
add_contact_search: String,
|
||||
add_contact_searched: Option<String>,
|
||||
add_contact_search_results: Vec<(String, PublicKey)>,
|
||||
add_contact_search_selected: Option<usize>,
|
||||
|
||||
configure_list_menu_active: bool,
|
||||
entering_follow_someone_on_list: bool,
|
||||
clear_list_needs_confirm: bool,
|
||||
}
|
||||
|
||||
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(),
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn enter_page(app: &mut GossipUi, list: PersonList) {
|
||||
refresh_list_data(app, list);
|
||||
}
|
||||
|
||||
pub(super) fn update(
|
||||
app: &mut GossipUi,
|
||||
ctx: &Context,
|
||||
@ -14,63 +63,68 @@ pub(super) fn update(
|
||||
ui: &mut Ui,
|
||||
list: PersonList,
|
||||
) {
|
||||
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
|
||||
};
|
||||
|
||||
ui.add_space(12.0);
|
||||
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
let txt = 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
|
||||
)
|
||||
};
|
||||
// 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);
|
||||
}
|
||||
|
||||
ui.label(RichText::new(txt).size(15.0))
|
||||
.on_hover_text("This is the data in the latest list event fetched from relays");
|
||||
// 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;
|
||||
|
||||
ui.add_space(10.0);
|
||||
// render page
|
||||
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);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(30.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;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
ui.set_enabled(enabled);
|
||||
|
||||
if GLOBALS.signer.is_ready() {
|
||||
ui.vertical(|ui| {
|
||||
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);
|
||||
|
||||
// remote <-> local buttons
|
||||
ui.horizontal(|ui|{
|
||||
if ui
|
||||
.button("↓ Overwrite ↓")
|
||||
.on_hover_text(
|
||||
@ -109,52 +163,16 @@ pub(super) fn update(
|
||||
.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);
|
||||
ui.add_space(5.0);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
ui.label(RichText::new(format!("LOCAL: {} (size={})", ledit, people.len())).size(15.0))
|
||||
// local timestamp
|
||||
ui.label(RichText::new(&app.people_list.cache_local_tag))
|
||||
.on_hover_text("This is the local (and effective) list");
|
||||
|
||||
if !GLOBALS.signer.is_ready() {
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
});
|
||||
} else {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("You need to ");
|
||||
if ui.link("setup your identity").clicked() {
|
||||
app.set_page(ctx, Page::YourKeys);
|
||||
@ -165,20 +183,11 @@ 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| {
|
||||
// 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) {
|
||||
@ -186,13 +195,20 @@ pub(super) fn update(
|
||||
} else {
|
||||
app.placeholder_avatar.clone()
|
||||
};
|
||||
if widgets::paint_avatar(ui, person, &avatar, widgets::AvatarSize::Feed).clicked() {
|
||||
app.set_page(ctx, Page::Person(person.pubkey));
|
||||
};
|
||||
let avatar_height =
|
||||
widgets::paint_avatar(ui, person, &avatar, widgets::AvatarSize::Feed)
|
||||
.rect
|
||||
.height();
|
||||
|
||||
ui.add_space(20.0);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.label(RichText::new(gossip_lib::names::pubkey_short(&person.pubkey)).weak());
|
||||
GossipUi::render_person_name_line(app, ui, person, false);
|
||||
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)
|
||||
@ -203,8 +219,34 @@ pub(super) fn update(
|
||||
.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,
|
||||
@ -212,76 +254,305 @@ pub(super) fn update(
|
||||
!*public,
|
||||
None,
|
||||
);
|
||||
mark_refresh(app);
|
||||
}
|
||||
ui.label(if *public { "public" } else { "private" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if ui.button("Remove").clicked() {
|
||||
let _ = GLOBALS
|
||||
.storage
|
||||
.remove_person_from_list(&person.pubkey, list, None);
|
||||
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 {
|
||||
const DLG_SIZE: Vec2 = vec2(400.0, 200.0);
|
||||
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| {
|
||||
ui.heading("Follow someone");
|
||||
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("Add contact to the list");
|
||||
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, nprofile or nip-05 address");
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Enter");
|
||||
ui.add(
|
||||
text_edit_line!(app, app.follow_someone)
|
||||
text_edit_multiline!(app, app.add_contact)
|
||||
.desired_width(f32::INFINITY)
|
||||
.hint_text("npub1, hex key, nprofile1, or user@domain"),
|
||||
);
|
||||
});
|
||||
if ui.button("follow").clicked() {
|
||||
|
||||
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 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.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.entering_follow_someone_on_list = false;
|
||||
can_close = 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, list, true));
|
||||
app.entering_follow_someone_on_list = false;
|
||||
can_close = 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.clone(),
|
||||
list,
|
||||
true,
|
||||
));
|
||||
app.entering_follow_someone_on_list = false;
|
||||
} else if gossip_lib::nip05::parse_nip05(app.follow_someone.trim()).is_ok() {
|
||||
can_close = true;
|
||||
} 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,
|
||||
));
|
||||
} else {
|
||||
add_failed = true;
|
||||
GLOBALS
|
||||
.status_queue
|
||||
.write()
|
||||
.write("Invalid pubkey.".to_string());
|
||||
}
|
||||
app.follow_someone = "".to_owned();
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
if ret.inner.clicked() {
|
||||
app.entering_follow_someone_on_list = false;
|
||||
}
|
||||
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| {
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.inner
|
||||
.clicked()
|
||||
{
|
||||
app.people_list.clear_list_needs_confirm = 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 = "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]"
|
||||
)) {
|
||||
asof = formatted;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
} 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 = "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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -6,6 +6,18 @@ mod list;
|
||||
mod lists;
|
||||
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);
|
||||
|
@ -15,12 +15,12 @@ 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);
|
||||
widgets::search_filter_field(ui, &mut app.relays.search, 200.0);
|
||||
btn_h_space!(ui);
|
||||
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()))
|
||||
|
@ -11,12 +11,12 @@ 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);
|
||||
widgets::search_filter_field(ui, &mut app.relays.search, 200.0);
|
||||
btn_h_space!(ui);
|
||||
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
|
||||
|
@ -12,12 +12,12 @@ 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);
|
||||
widgets::search_filter_field(ui, &mut app.relays.search, 200.0);
|
||||
btn_h_space!(ui);
|
||||
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")
|
||||
|
@ -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,63 +448,13 @@ 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;
|
||||
}
|
||||
|
||||
let button_center_bottom = response.rect.center_bottom();
|
||||
let seen_on_popup_position = button_center_bottom + vec2(-180.0, widgets::DROPDOWN_DISTANCE);
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
// since we are displaying over an accent color background, load that style
|
||||
@ -521,10 +471,6 @@ pub(super) fn configure_list_btn(app: &mut GossipUi, ui: &mut Ui) {
|
||||
});
|
||||
});
|
||||
});
|
||||
if menuresp.response.clicked_elsewhere() && !response.clicked() {
|
||||
app.relays.configure_list_menu_active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
|
170
gossip-bin/src/ui/widgets/contact_search.rs
Normal file
170
gossip-bin/src/ui/widgets/contact_search.rs
Normal file
@ -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<usize>,
|
||||
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<usize>,
|
||||
) -> (Option<usize>, 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)
|
||||
})
|
||||
}
|
@ -1,13 +1,17 @@
|
||||
mod avatar;
|
||||
pub(crate) use avatar::{paint_avatar, AvatarSize};
|
||||
|
||||
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,
|
||||
self, vec2, FontSelection, Rect, Sense, TextEdit, Ui, WidgetText,
|
||||
};
|
||||
pub use nav_item::NavItem;
|
||||
|
||||
@ -17,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;
|
||||
@ -87,16 +94,16 @@ pub fn break_anywhere_hyperlink_to(ui: &mut Ui, text: impl Into<WidgetText>, 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)
|
||||
let output = TextEdit::singleline(field)
|
||||
.text_color(ui.visuals().widgets.inactive.fg_stroke.color)
|
||||
.desired_width(width),
|
||||
);
|
||||
.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
|
||||
@ -114,7 +121,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) {
|
||||
|
117
gossip-bin/src/ui/widgets/more_menu.rs
Normal file
117
gossip-bin/src/ui/widgets/more_menu.rs
Normal file
@ -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<String>,
|
||||
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<Id>) -> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user