Merge remote-tracking branch 'bushmann/feature/relay-list-widget' into unstable

This commit is contained in:
Mike Dilger 2023-08-19 08:39:41 +12:00
commit 1e4ecc888d
20 changed files with 2431 additions and 487 deletions

3
assets/option.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 17 17">
<path fill="#FFFFFFFF" fill-rule="evenodd" d="M9.79 4.045H.99a.8.8 0 0 1 0-1.6h8.8a1.6 1.6 0 0 1 1.6-1.6h1.6a1.6 1.6 0 0 1 1.6 1.6h.8a.8.8 0 0 1 0 1.6h-.8a1.6 1.6 0 0 1-1.6 1.6h-1.6a1.6 1.6 0 0 1-1.6-1.6Zm3.2-1.6h-1.6v1.6h1.6v-1.6Zm2.4 5.6h-7.2a1.6 1.6 0 0 0-1.6-1.6h-1.6a1.6 1.6 0 0 0-1.6 1.6H.99a.8.8 0 1 0 0 1.6h2.4a1.6 1.6 0 0 0 1.6 1.6h1.6a1.6 1.6 0 0 0 1.6-1.6h7.2a.8.8 0 0 0 0-1.6Zm-8.8 0h-1.6v1.6h1.6v-1.6Zm8.8 5.6h-2.4a1.6 1.6 0 0 0-1.6-1.6h-1.6a1.6 1.6 0 0 0-1.6 1.6H.99a.8.8 0 0 0 0 1.6h7.2a1.6 1.6 0 0 0 1.6 1.6h1.6a1.6 1.6 0 0 0 1.6-1.6h2.4a.8.8 0 0 0 0-1.6Zm-4 0h-1.6v1.6h1.6v-1.6Z" clip-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 712 B

View File

@ -34,6 +34,7 @@ pub enum ToOverlordMessage {
PushMetadata(Metadata),
ReengageMinion(RelayUrl, Vec<RelayJob>),
RefreshFollowedMetadata,
ClearAllUsageOnRelay(RelayUrl),
Repost(Id),
RankRelay(RelayUrl, u8),
SaveSettings,

View File

@ -482,6 +482,21 @@ impl Overlord {
let dbrelay = Relay::new(relay_str);
GLOBALS.storage.write_relay(&dbrelay, None)?;
}
ToOverlordMessage::ClearAllUsageOnRelay(relay_url) => {
if let Some(mut dbrelay) = GLOBALS.storage.read_relay(&relay_url)? {
// TODO: replace with dedicated method to clear all bits
dbrelay.clear_usage_bits(
Relay::ADVERTISE
| Relay::DISCOVER
| Relay::INBOX
| Relay::OUTBOX
| Relay::READ
| Relay::WRITE,
);
} else {
tracing::error!("CODE OVERSIGHT - Attempt to clear relay usage bit for a relay not in memory. It will not be saved.");
}
}
ToOverlordMessage::AdjustRelayUsageBit(relay_url, bit, value) => {
if let Some(mut dbrelay) = GLOBALS.storage.read_relay(&relay_url)? {
dbrelay.adjust_usage_bit(bit, value);

View File

@ -1,9 +1,6 @@
mod nav_item;
use eframe::egui;
use egui::{Label, Response, Sense, Ui};
pub use nav_item::NavItem;
use egui_winit::egui::{Color32, Id, Rect, Stroke};
pub fn emoji_picker(ui: &mut Ui) -> Option<char> {
let mut emojis = "😀😁😆😅😂🤣\
@ -45,11 +42,26 @@ pub fn emoji_picker(ui: &mut Ui) -> Option<char> {
}
pub fn switch_with_size(ui: &mut Ui, on: &mut bool, size: egui::Vec2) -> Response {
let (rect, mut response) = ui.allocate_exact_size(size, egui::Sense::click());
let (rect, _) = ui.allocate_exact_size(size, egui::Sense::click());
switch_with_size_at(ui, on, size, rect.left_top(), ui.next_auto_id())
}
pub fn switch_with_size_at(
ui: &mut Ui,
on: &mut bool,
size: egui::Vec2,
pos: egui::Pos2,
id: Id,
) -> Response {
let rect = Rect::from_min_size(pos, size);
let mut response = ui.interact(rect, id, egui::Sense::click());
if response.clicked() {
*on = !*on;
response.mark_changed();
}
response
.clone()
.on_hover_cursor(egui::CursorIcon::PointingHand);
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
if ui.is_rect_visible(rect) {
@ -75,3 +87,71 @@ pub fn switch_with_size(ui: &mut Ui, on: &mut bool, size: egui::Vec2) -> Respons
response
}
#[allow(clippy::too_many_arguments)]
pub fn switch_custom_at(
ui: &mut Ui,
enabled: bool,
value: &mut bool,
rect: Rect,
id: Id,
knob_fill: Color32,
on_fill: Color32,
off_fill: Color32,
) -> Response {
let sense = if enabled {
egui::Sense::click()
} else {
egui::Sense::hover()
};
let mut response = ui.interact(rect, id, sense);
if response.clicked() {
*value = !*value;
response.mark_changed();
}
response = if enabled {
response.on_hover_cursor(egui::CursorIcon::PointingHand)
} else {
response
};
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *value, ""));
if ui.is_rect_visible(rect) {
let how_on = ui.ctx().animate_bool(response.id, *value);
let visuals = if enabled {
ui.style().interact_selectable(&response, *value)
} else {
ui.visuals().widgets.inactive
};
// skip expansion, keep tight
//let rect = rect.expand(visuals.expansion);
let radius = 0.5 * rect.height();
// bg_fill, bg_stroke, fg_stroke, expansion
let bg_fill = if !enabled {
visuals.bg_fill
} else if *value {
on_fill
} else {
off_fill
};
let fg_stroke = if enabled {
visuals.fg_stroke
} else {
visuals.bg_stroke
};
ui.painter()
.rect(rect.shrink(1.0), radius, bg_fill, visuals.bg_stroke);
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
let center = egui::pos2(circle_x, rect.center().y);
ui.painter().circle(
center,
0.875 * radius,
knob_fill,
Stroke::new(0.7, fg_stroke.color),
);
}
response
}

View File

@ -303,10 +303,8 @@ fn show_image_toggle(app: &mut GossipUi, ui: &mut Ui, url: Url) {
}
if show_link {
let response = ui.link("[ Image ]");
// show url on hover
response.clone().on_hover_text(url_string.clone());
// show media toggle
let response = ui.link("[ Image ]").on_hover_text(url_string.clone()); // show url on hover
// show media toggle
if response.clicked() {
if app.settings.show_media {
app.media_hide_list.remove(&url);
@ -411,10 +409,8 @@ fn show_video_toggle(app: &mut GossipUi, ui: &mut Ui, url: Url) {
}
if show_link {
let response = ui.link("[ Video ]");
// show url on hover
response.clone().on_hover_text(url_string.clone());
// show media toggle
let response = ui.link("[ Video ]").on_hover_text(url_string.clone()); // show url on hover
// show media toggle
if response.clicked() {
if app.settings.show_media {
app.media_hide_list.remove(&url);
@ -549,7 +545,7 @@ fn add_media_menu(app: &mut GossipUi, ui: &mut Ui, url: Url, response: &Response
};
let extend_area = extend_area.expand(SPACE * 2.0);
if let Some(pointer_pos) = ui.ctx().pointer_latest_pos() {
if extend_area.contains(pointer_pos) {
if extend_area.contains(pointer_pos) && ui.is_enabled() {
ui.add_space(SPACE);
ui.vertical(|ui| {
ui.add_space(SPACE);

View File

@ -92,7 +92,7 @@ pub(in crate::ui) fn posting_area(
ui.horizontal_wrapped(|ui| {
ui.label("You need to ");
if ui.link("choose write relays").clicked() {
app.set_page(Page::RelaysAll);
app.set_page(Page::RelaysKnownNetwork);
}
ui.label(" to post.");
});

View File

@ -57,7 +57,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
ui.horizontal_wrapped(|ui| {
ui.label("On the");
if ui.link("Relays > Configure").clicked() {
app.set_page(Page::RelaysAll);
app.set_page(Page::RelaysKnownNetwork);
}
ui.label("page, add a few relays that you post to and read from, and tick the \"Read\" and \"Write\" columns appropriately.\n\nWRITE RELAYS: These are used for writing posts, and for reading back your posts including your RelayList and ContactList, which we will need to get started. You should have published these from a previous client, and you should specify a relay that has these on it.\n\nREAD RELAYS: These are used to find other people's RelayList (including those embedded in ContactList events), as a fallback for users that gossip has not found yet, and more. Once gossip learns where the people you follow post, it will pick up their posts from their write relays rather than from your read relays. The more read relays you configure, the better chance you'll find everybody.");
});
@ -81,7 +81,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
ui.horizontal_wrapped(|ui| {
ui.label("On the");
if ui.link("Relays > Live").clicked() {
app.set_page(Page::RelaysLive);
app.set_page(Page::RelaysActivityMonitor);
}
ui.label("page, watch the live connections. Press [Pick Again] if connections aren't being made. If people aren't being found, you may need to add different relays and try this again. Watch the console output to see if gossip is busy and wait for it to settle down a bit.");
});
@ -153,7 +153,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
ui.horizontal_wrapped(|ui| {
ui.label("On the");
if ui.link("Relays > Configure").clicked() {
app.set_page(Page::RelaysAll);
app.set_page(Page::RelaysKnownNetwork);
}
ui.label("page, add a few relays that you post to and read from, and tick the \"Read\" and \"Write\" columns appropriately. You will need to search the Internet for nostr relays as we don't want to give special mention to any in particular.\n\nWRITE RELAYS: These are used for writing posts, and for reading back your posts including your RelayList and ContactList whenever you move clients.\n\nREAD RELAYS: These are used to find other people's RelayList (including those embedded in ContactList events), as a fallback for users that gossip has not found yet, and more. Once gossip learns where the people you follow post, it will pick up their posts from their write relays rather than from your read relays. The more read relays you configure, the better chance you'll find everybody.");
});
@ -177,7 +177,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
ui.horizontal_wrapped(|ui| {
ui.label("On the");
if ui.link("Relays > Live").clicked() {
app.set_page(Page::RelaysLive);
app.set_page(Page::RelaysActivityMonitor);
}
ui.label("page, watch the live connections. Press [Pick Again] if connections aren't being made. If people aren't being found, you may need to add different relays and try this again. Watch the console output to see if gossip is busy and wait for it to settle down a bit.");
});

View File

@ -50,8 +50,8 @@ use std::sync::atomic::Ordering;
use std::time::{Duration, Instant};
use zeroize::Zeroize;
use self::components::NavItem;
use self::feed::Notes;
use self::widgets::NavItem;
pub fn run() -> Result<(), Error> {
let icon_bytes = include_bytes!("../../gossip.png");
@ -102,8 +102,9 @@ enum Page {
YourKeys,
YourMetadata,
YourDelegation,
RelaysLive,
RelaysAll,
RelaysActivityMonitor,
RelaysMine,
RelaysKnownNetwork,
Search,
Settings,
HelpHelp,
@ -189,6 +190,9 @@ struct GossipUi {
// Processed events caching
notes: Notes,
// RelayUi
relays: relays::RelayUi,
// Post rendering
render_raw: Option<Id>,
render_qr: Option<Id>,
@ -212,6 +216,7 @@ struct GossipUi {
about: About,
icon: TextureHandle,
placeholder_avatar: TextureHandle,
options_symbol: TextureHandle,
settings: Settings,
avatars: HashMap<PublicKey, TextureHandle>,
images: HashMap<Url, TextureHandle>,
@ -255,8 +260,6 @@ struct GossipUi {
new_metadata_fieldname: String,
import_priv: String,
import_pub: String,
new_relay_url: String,
show_hidden_relays: bool,
search: String,
entering_search_page: bool,
@ -366,17 +369,19 @@ impl GossipUi {
};
// how to load an svg
// let expand_right_symbol = {
// let bytes = include_bytes!("../../assets/expand-image.svg");
// let color_image = egui_extras::image::load_svg_bytes_with_size(
// bytes,
// egui_extras::image::FitTo::Size(200, 1000),
// ).unwrap();
// cctx.egui_ctx.load_texture(
// "expand_right_symbol",
// color_image,
// TextureOptions::default())
// };
let options_symbol = {
let bytes = include_bytes!("../../assets/option.svg");
let color_image = egui_extras::image::load_svg_bytes_with_size(
bytes,
egui_extras::image::FitTo::Size(
(cctx.egui_ctx.pixels_per_point() * 40.0) as u32,
(cctx.egui_ctx.pixels_per_point() * 40.0) as u32,
),
)
.unwrap();
cctx.egui_ctx
.load_texture("options_symbol", color_image, TextureOptions::LINEAR)
};
let current_dpi = (cctx.egui_ctx.pixels_per_point() * 72.0) as u32;
let (override_dpi, override_dpi_value): (bool, u32) = match settings.override_dpi {
@ -409,6 +414,7 @@ impl GossipUi {
future_scroll_offset: 0.0,
qr_codes: HashMap::new(),
notes: Notes::new(),
relays: relays::RelayUi::new(),
render_raw: None,
render_qr: None,
approved: HashSet::new(),
@ -431,6 +437,7 @@ impl GossipUi {
about: crate::about::about(),
icon: icon_texture_handle,
placeholder_avatar: placeholder_avatar_texture_handle,
options_symbol,
settings,
avatars: HashMap::new(),
images: HashMap::new(),
@ -463,8 +470,6 @@ impl GossipUi {
new_metadata_fieldname: String::new(),
import_priv: "".to_owned(),
import_pub: "".to_owned(),
new_relay_url: "".to_owned(),
show_hidden_relays: false,
search: "".to_owned(),
entering_search_page: false,
collapsed: vec![],
@ -607,6 +612,11 @@ impl eframe::App for GossipUi {
}
}
// dialogues first
if relays::is_entry_dialog_active(self) {
relays::entry_dialog(ctx, self);
}
if self.settings.status_bar {
egui::TopBottomPanel::top("stats-bar")
.frame(
@ -669,6 +679,8 @@ impl eframe::App for GossipUi {
.fill(self.settings.theme.navigation_bg_fill())
)
.show(ctx, |ui| {
self.begin_ui(ui);
// cut indentation
ui.style_mut().spacing.indent = 0.0;
ui.style_mut().visuals.widgets.inactive.fg_stroke.color = self.settings.theme.navigation_text_color();
@ -744,8 +756,19 @@ impl eframe::App for GossipUi {
let (mut submenu, header_response) =
self.get_openable_menu( ui, SubMenu::Relays, "Relays");
submenu.show_body_indented(&header_response, ui, |ui| {
self.add_menu_item_page(ui, Page::RelaysLive, "Live");
self.add_menu_item_page(ui, Page::RelaysAll, "Configure");
self.add_menu_item_page(ui, Page::RelaysActivityMonitor, "Active Relays");
self.add_menu_item_page(ui, Page::RelaysMine, "My Relays");
self.add_menu_item_page(ui, Page::RelaysKnownNetwork, "Known Network");
ui.vertical(|ui| {
ui.spacing_mut().button_padding *= 2.0;
ui.visuals_mut().widgets.inactive.weak_bg_fill = self.settings.theme.accent_color().linear_multiply(0.2);
ui.visuals_mut().widgets.inactive.fg_stroke.width = 1.0;
ui.visuals_mut().widgets.hovered.weak_bg_fill = self.settings.theme.navigation_text_color();
ui.visuals_mut().widgets.hovered.fg_stroke.color = self.settings.theme.accent_color();
if ui.button(RichText::new("Add Relay")).on_hover_cursor(egui::CursorIcon::PointingHand).clicked() {
relays::start_entry_dialog(self);
}
});
});
self.after_openable_menu(ui, &submenu);
}
@ -819,6 +842,7 @@ impl eframe::App for GossipUi {
.fixed_pos(pos)
.constrain(true)
.show(ctx, |ui| {
self.begin_ui(ui);
egui::Frame::popup(&self.settings.theme.get_style())
.rounding(egui::Rounding::same(crate::AVATAR_SIZE_F32/2.0)) // need the rounding for the shadow
.stroke(egui::Stroke::NONE)
@ -870,6 +894,7 @@ impl eframe::App for GossipUi {
ctx,
self.show_post_area && self.settings.posting_area_at_top,
|ui| {
self.begin_ui(ui);
feed::post::posting_area(self, ctx, frame, ui);
},
);
@ -900,6 +925,7 @@ impl eframe::App for GossipUi {
.resizable(resizable)
.show_separator_line(false)
.show_animated(ctx, show_status, |ui| {
self.begin_ui(ui);
if self.show_post_area && !self.settings.posting_area_at_top {
ui.add_space(7.0);
feed::post::posting_area(self, ctx, frame, ui);
@ -926,25 +952,35 @@ impl eframe::App for GossipUi {
bottom: 0.0,
})
})
.show(ctx, |ui| match self.page {
Page::Feed(_) => feed::update(self, ctx, frame, ui),
Page::PeopleList | Page::PeopleFollow | Page::PeopleMuted | Page::Person(_) => {
people::update(self, ctx, frame, ui)
}
Page::YourKeys | Page::YourMetadata | Page::YourDelegation => {
you::update(self, ctx, frame, ui)
}
Page::RelaysLive | Page::RelaysAll => relays::update(self, ctx, frame, ui),
Page::Search => search::update(self, ctx, frame, ui),
Page::Settings => settings::update(self, ctx, frame, ui),
Page::HelpHelp | Page::HelpStats | Page::HelpAbout => {
help::update(self, ctx, frame, ui)
.show(ctx, |ui| {
self.begin_ui(ui);
match self.page {
Page::Feed(_) => feed::update(self, ctx, frame, ui),
Page::PeopleList | Page::PeopleFollow | Page::PeopleMuted | Page::Person(_) => {
people::update(self, ctx, frame, ui)
}
Page::YourKeys | Page::YourMetadata | Page::YourDelegation => {
you::update(self, ctx, frame, ui)
}
Page::RelaysActivityMonitor | Page::RelaysMine | Page::RelaysKnownNetwork => {
relays::update(self, ctx, frame, ui)
}
Page::Search => search::update(self, ctx, frame, ui),
Page::Settings => settings::update(self, ctx, frame, ui),
Page::HelpHelp | Page::HelpStats | Page::HelpAbout => {
help::update(self, ctx, frame, ui)
}
}
});
}
}
impl GossipUi {
fn begin_ui(&self, ui: &mut Ui) {
// if a dialog is open, disable the rest of the UI
ui.set_enabled(!relays::is_entry_dialog_active(self));
}
/// A short rendering of a `PublicKey`
pub fn pubkey_short(pk: &PublicKey) -> String {
let npub = pk.as_bech32_string();
@ -1255,9 +1291,6 @@ impl GossipUi {
let response = ui.add(label);
ui.add_space(2.0);
response
.clone()
.on_hover_cursor(egui::CursorIcon::PointingHand);
response
}
fn handle_visible_note_changes(&mut self) {

102
src/ui/relays/active.rs Normal file
View File

@ -0,0 +1,102 @@
use std::collections::HashSet;
use super::GossipUi;
use crate::comms::ToOverlordMessage;
use crate::globals::GLOBALS;
use crate::relay::Relay;
use crate::ui::widgets;
use eframe::egui;
use egui::{Context, Ui};
use egui_winit::egui::Id;
use nostr_types::RelayUrl;
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
let is_editing = app.relays.edit.is_some();
ui.add_space(10.0);
ui.horizontal_wrapped(|ui| {
ui.heading("Active Relays");
ui.set_enabled(!is_editing);
ui.add_space(10.0);
if ui.button("Pick Again").clicked() {
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PickRelays);
}
ui.add_space(50.0);
widgets::search_filter_field(ui, &mut app.relays.search, 200.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
ui.add_space(20.0);
super::configure_list_btn(app, ui);
ui.add_space(20.0);
super::relay_filter_combo(app, ui);
ui.add_space(20.0);
super::relay_sort_combo(app, ui);
});
});
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
ui.heading("Coverage");
if GLOBALS.relay_picker.pubkey_counts_iter().count() > 0 {
for elem in GLOBALS.relay_picker.pubkey_counts_iter() {
let pk = elem.key();
let count = elem.value();
let name = GossipUi::display_name_from_pubkey_lookup(pk);
ui.label(format!("{}: coverage short by {} relay(s)", name, count));
}
ui.add_space(12.0);
if ui.button("Pick Again").clicked() {
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PickRelays);
}
} else {
ui.label("All followed people are fully covered.".to_owned());
}
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
let relays = if !is_editing {
// clear edit cache if present
if !app.relays.edit_relays.is_empty() {
app.relays.edit_relays.clear()
}
get_relays(app)
} else {
// when editing, use cached list
// build list if still empty
if app.relays.edit_relays.is_empty() {
app.relays.edit_relays = get_relays(app);
}
app.relays.edit_relays.clone()
};
let id_source: Id = "RelayActivityMonitorScroll".into();
super::relay_scroll_list(app, ui, relays, id_source);
}
fn get_relays(app: &mut GossipUi) -> Vec<Relay> {
let connected_relays: HashSet<RelayUrl> = GLOBALS
.connected_relays
.iter()
.map(|r| r.key().clone())
.collect();
let timeout_relays: HashSet<RelayUrl> = GLOBALS
.relay_picker
.excluded_relays_iter()
.map(|r| r.key().clone())
.collect();
let mut relays: Vec<Relay> = GLOBALS
.storage
.filter_relays(|relay| {
(connected_relays.contains(&relay.url) || timeout_relays.contains(&relay.url))
&& super::filter_relay(&app.relays, relay)
})
.unwrap_or(Vec::new());
relays.sort_by(|a, b| super::sort_relay(&app.relays, a, b));
relays
}

View File

@ -1,252 +0,0 @@
use super::GossipUi;
use crate::comms::ToOverlordMessage;
use crate::globals::GLOBALS;
use crate::relay::Relay;
use eframe::egui;
use egui::{Align, Context, Layout, Ui};
use egui_extras::{Column, TableBuilder};
use nostr_types::{RelayUrl, Unixtime};
const READ_HOVER_TEXT: &str = "Where you actually read events from (including those tagging you, but also for other purposes).";
const INBOX_HOVER_TEXT: &str = "Where you tell others you read from. You should also check Read. These relays shouldn't require payment. It is recommended to have a few.";
const DISCOVER_HOVER_TEXT: &str = "Where you discover other people's relays lists.";
const WRITE_HOVER_TEXT: &str =
"Where you actually write your events to. It is recommended to have a few.";
const OUTBOX_HOVER_TEXT: &str = "Where you tell others you write to. You should also check Write. It is recommended to have a few.";
const ADVERTISE_HOVER_TEXT: &str = "Where you advertise your relay list (inbox/outbox) to. It is recommended to advertise to lots of relays so that you can be found.";
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
ui.add_space(16.0);
ui.heading("Relays List");
ui.horizontal(|ui| {
ui.label("Enter a new relay URL:");
ui.add(text_edit_line!(app, app.new_relay_url));
if ui.button("Add").clicked() {
if let Ok(url) = RelayUrl::try_from_str(&app.new_relay_url) {
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::AddRelay(url));
GLOBALS.status_queue.write().write(format!(
"I asked the overlord to add relay {}. Check for it below.",
&app.new_relay_url
));
app.new_relay_url = "".to_owned();
} else {
GLOBALS
.status_queue
.write()
.write("That's not a valid relay URL.".to_owned());
}
}
ui.separator();
if ui.button("↑ Advertise Relay List ↑").clicked() {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::AdvertiseRelayList);
}
ui.checkbox(&mut app.show_hidden_relays, "Show hidden relays");
});
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
// TBD time how long this takes. We don't want expensive code in the UI
// FIXME keep more relay info and display it
let mut relays: Vec<Relay> = GLOBALS
.storage
.filter_relays(|relay| app.show_hidden_relays || !relay.hidden)
.unwrap_or(vec![]);
relays.sort_by(|a, b| {
b.has_usage_bits(Relay::WRITE)
.cmp(&a.has_usage_bits(Relay::WRITE))
.then(a.url.cmp(&b.url))
});
ui.with_layout(Layout::bottom_up(Align::Center), |ui| {
ui.add_space(18.0);
ui.with_layout(Layout::top_down(Align::Min), |ui| {
ui.heading("All Known Relays:");
relay_table(ui, &mut relays, "allrelays");
});
});
}
fn relay_table(ui: &mut Ui, relays: &mut [Relay], id: &'static str) {
ui.push_id(id, |ui| {
TableBuilder::new(ui)
.striped(true)
.column(Column::auto_with_initial_suggestion(250.0).resizable(true))
.column(Column::auto().resizable(true))
.column(Column::auto().resizable(true))
.column(Column::auto().resizable(true))
.column(Column::auto().resizable(true))
.column(Column::auto().resizable(true))
.column(Column::auto().resizable(true))
.column(Column::auto().resizable(true))
.column(Column::auto().resizable(true))
.column(Column::auto().resizable(true))
.column(Column::auto().resizable(true))
.column(Column::auto().resizable(true))
.column(Column::remainder())
.header(20.0, |mut header| {
header.col(|ui| {
ui.heading("Relay URL");
});
header.col(|ui| {
ui.heading("Attempts");
});
header.col(|ui| {
ui.heading("Success Rate (%)");
});
header.col(|ui| {
ui.heading("Last Connected");
});
header.col(|ui| {
ui.heading("Last Event")
.on_hover_text("This only counts events served after EOSE, as they mark where we can pick up from next time.");
});
header.col(|ui| {
ui.heading("Read").on_hover_text(READ_HOVER_TEXT);
});
header.col(|ui| {
ui.heading("Inbox").on_hover_text(INBOX_HOVER_TEXT);
});
header.col(|ui| {
ui.heading("Discover").on_hover_text(DISCOVER_HOVER_TEXT);
});
header.col(|ui| {
ui.heading("Write").on_hover_text(WRITE_HOVER_TEXT);
});
header.col(|ui| {
ui.heading("Outbox").on_hover_text(OUTBOX_HOVER_TEXT);
});
header.col(|ui| {
ui.heading("Advertise").on_hover_text(ADVERTISE_HOVER_TEXT);
});
header.col(|ui| {
ui.heading("Read rank")
.on_hover_text("How likely we will connect to relays to read other people's posts, from 0 (never) to 9 (highly). Default is 3.".to_string());
});
header.col(|ui| {
ui.heading("Hide")
.on_hover_text("Hide this relay.".to_string());
});
}).body(|body| {
body.rows(24.0, relays.len(), |row_index, mut row| {
let relay = relays.get_mut(row_index).unwrap();
row.col(|ui| {
crate::ui::widgets::break_anywhere_label(ui,&relay.url.0);
});
row.col(|ui| {
ui.label(&format!("{}", relay.attempts()));
});
row.col(|ui| {
ui.label(&format!("{}", (relay.success_rate() * 100.0) as u32));
});
row.col(|ui| {
if let Some(at) = relay.last_connected_at {
let ago = crate::date_ago::date_ago(Unixtime(at as i64));
ui.label(&ago);
}
});
row.col(|ui| {
if let Some(at) = relay.last_general_eose_at {
let ago = crate::date_ago::date_ago(Unixtime(at as i64));
ui.label(&ago);
}
});
row.col(|ui| {
let mut read = relay.has_usage_bits(Relay::READ); // checkbox needs a mutable state variable.
if ui.checkbox(&mut read, "")
.on_hover_text(READ_HOVER_TEXT)
.clicked()
{
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::AdjustRelayUsageBit(relay.url.clone(), Relay::READ, read));
}
});
row.col(|ui| {
let mut inbox = relay.has_usage_bits(Relay::INBOX); // checkbox needs a mutable state variable.
if ui.checkbox(&mut inbox, "")
.on_hover_text(INBOX_HOVER_TEXT)
.clicked()
{
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::AdjustRelayUsageBit(relay.url.clone(), Relay::INBOX, inbox));
}
});
row.col(|ui| {
let mut discover = relay.has_usage_bits(Relay::DISCOVER); // checkbox needs a mutable state variable.
if ui.checkbox(&mut discover, "")
.on_hover_text(DISCOVER_HOVER_TEXT)
.clicked()
{
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::AdjustRelayUsageBit(relay.url.clone(), Relay::DISCOVER, discover));
}
});
row.col(|ui| {
let mut write = relay.has_usage_bits(Relay::WRITE); // checkbox needs a mutable state variable.
if ui.checkbox(&mut write, "")
.on_hover_text(WRITE_HOVER_TEXT)
.clicked()
{
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::AdjustRelayUsageBit(relay.url.clone(), Relay::WRITE, write));
}
});
row.col(|ui| {
let mut outbox = relay.has_usage_bits(Relay::OUTBOX); // checkbox needs a mutable state variable.
if ui.checkbox(&mut outbox, "")
.on_hover_text(OUTBOX_HOVER_TEXT)
.clicked()
{
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::AdjustRelayUsageBit(relay.url.clone(), Relay::OUTBOX, outbox));
}
});
row.col(|ui| {
let mut advertise = relay.has_usage_bits(Relay::ADVERTISE); // checkbox needs a mutable state variable.
if ui.checkbox(&mut advertise, "")
.on_hover_text(ADVERTISE_HOVER_TEXT)
.clicked()
{
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::AdjustRelayUsageBit(relay.url.clone(), Relay::ADVERTISE, advertise));
}
});
row.col(|ui| {
ui.horizontal(|ui| {
ui.label(format!("{}",relay.rank));
if ui.button("").clicked() && relay.rank>0 {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::RankRelay(relay.url.clone(), relay.rank as u8 - 1));
}
if ui.button("").clicked() && relay.rank<9 {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::RankRelay(relay.url.clone(), relay.rank as u8 + 1));
}
});
});
row.col(|ui| {
let icon = if relay.hidden { "♻️" } else { "🗑️" };
if ui.button(icon).clicked() {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::HideOrShowRelay(relay.url.clone(), !relay.hidden));
}
});
})
});
});
}

85
src/ui/relays/known.rs Normal file
View File

@ -0,0 +1,85 @@
use super::GossipUi;
use crate::globals::GLOBALS;
use crate::relay::Relay;
use crate::ui::widgets;
use eframe::egui;
use egui::{Context, Ui};
use egui_winit::egui::Id;
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
let is_editing = app.relays.edit.is_some();
ui.add_space(10.0);
ui.horizontal_wrapped(|ui| {
ui.heading("Known Relays");
ui.add_space(50.0);
ui.set_enabled(!is_editing);
widgets::search_filter_field(ui, &mut app.relays.search, 200.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
ui.add_space(20.0);
super::configure_list_btn(app, ui);
ui.add_space(20.0);
super::relay_filter_combo(app, ui);
ui.add_space(20.0);
super::relay_sort_combo(app, ui);
});
});
ui.add_space(10.0);
// ui.horizontal(|ui| {
// ui.label("Enter a new relay URL:");
// ui.add(text_edit_line!(app, app.new_relay_url));
// if ui.button("Add").clicked() {
// if let Ok(url) = RelayUrl::try_from_str(&app.new_relay_url) {
// let _ = GLOBALS.to_overlord.send(ToOverlordMessage::AddRelay(url));
// *GLOBALS.status_message.blocking_write() = format!(
// "I asked the overlord to add relay {}. Check for it below.",
// &app.new_relay_url
// );
// app.new_relay_url = "".to_owned();
// } else {
// *GLOBALS.status_message.blocking_write() =
// "That's not a valid relay URL.".to_owned();
// }
// }
// ui.separator();
// if ui.button("↑ Advertise Relay List ↑").clicked() {
// let _ = GLOBALS
// .to_overlord
// .send(ToOverlordMessage::AdvertiseRelayList);
// }
// ui.checkbox(&mut app.show_hidden_relays, "Show hidden relays");
// });
// TBD time how long this takes. We don't want expensive code in the UI
// FIXME keep more relay info and display it
let relays = if !is_editing {
// clear edit cache if present
if !app.relays.edit_relays.is_empty() {
app.relays.edit_relays.clear()
}
get_relays(app)
} else {
// when editing, use cached list
// build list if still empty
if app.relays.edit_relays.is_empty() {
app.relays.edit_relays = get_relays(app);
}
app.relays.edit_relays.clone()
};
let id_source: Id = "KnowRelaysScroll".into();
super::relay_scroll_list(app, ui, relays, id_source);
}
fn get_relays(app: &mut GossipUi) -> Vec<Relay> {
let mut relays: Vec<Relay> = GLOBALS
.storage
.filter_relays(|relay| {
app.relays.show_hidden || !relay.hidden && super::filter_relay(&app.relays, relay)
})
.unwrap_or(Vec::new());
relays.sort_by(|a, b| super::sort_relay(&app.relays, a, b));
relays
}

56
src/ui/relays/mine.rs Normal file
View File

@ -0,0 +1,56 @@
use super::GossipUi;
use crate::globals::GLOBALS;
use crate::relay::Relay;
use crate::ui::widgets;
use eframe::egui;
use egui::{Context, Ui};
use egui_winit::egui::Id;
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
let is_editing = app.relays.edit.is_some();
ui.add_space(10.0);
ui.horizontal_wrapped(|ui| {
ui.heading("My Relays");
ui.add_space(50.0);
ui.set_enabled(!is_editing);
widgets::search_filter_field(ui, &mut app.relays.search, 200.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
ui.add_space(20.0);
super::configure_list_btn(app, ui);
ui.add_space(20.0);
super::relay_filter_combo(app, ui);
ui.add_space(20.0);
super::relay_sort_combo(app, ui);
});
});
ui.add_space(10.0);
let relays = if !is_editing {
// clear edit cache if present
if !app.relays.edit_relays.is_empty() {
app.relays.edit_relays.clear()
}
get_relays(app)
} else {
// when editing, use cached list
// build list if still empty
if app.relays.edit_relays.is_empty() {
app.relays.edit_relays = get_relays(app);
}
app.relays.edit_relays.clone()
};
let id_source: Id = "MyRelaysScroll".into();
super::relay_scroll_list(app, ui, relays, id_source);
}
fn get_relays(app: &mut GossipUi) -> Vec<Relay> {
let mut relays: Vec<Relay> = GLOBALS
.storage
.filter_relays(|relay| relay.usage_bits != 0 && super::filter_relay(&app.relays, relay))
.unwrap_or(Vec::new());
relays.sort_by(|a, b| super::sort_relay(&app.relays, a, b));
relays
}

View File

@ -1,157 +1,639 @@
use std::cmp::Ordering;
use super::{GossipUi, Page};
use crate::comms::ToOverlordMessage;
use crate::globals::GLOBALS;
use crate::{comms::ToOverlordMessage, globals::GLOBALS, relay::Relay};
use eframe::egui;
use egui::{Context, ScrollArea, Ui, Vec2};
use egui_extras::{Column, TableBuilder};
use nostr_types::{RelayUrl, Unixtime};
use egui::{Context, Ui};
use egui_winit::egui::{vec2, Id, Rect, RichText};
use nostr_types::RelayUrl;
mod all;
mod active;
mod known;
mod mine;
pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Frame, ui: &mut Ui) {
if app.page == Page::RelaysLive {
ui.add_space(10.0);
pub(super) struct RelayUi {
/// text of search field
search: String,
/// how to sort relay entries
sort: RelaySorting,
/// which relays to include in the list
filter: RelayFilter,
/// Show hidden relays on/off
show_hidden: bool,
/// show details on/off
show_details: bool,
/// to edit, add the relay url here
edit: Option<RelayUrl>,
/// cache relay list for editing
edit_relays: Vec<Relay>,
/// did we just finish editing an entry, add it here
edit_done: Option<RelayUrl>,
/// do we still need to scroll to the edit
edit_needs_scroll: bool,
ui.heading("Connected Relays");
ui.add_space(18.0);
/// Add Relay dialog
add_dialog_step: AddRelayDialogStep,
new_relay_url: String,
let connected_relays: Vec<(RelayUrl, String)> = GLOBALS
.connected_relays
.iter()
.map(|r| {
(
r.key().clone(),
r.value()
.iter()
.map(|rj| {
if rj.persistent {
format!("[{}]", rj.reason)
} else {
rj.reason.to_string()
}
})
.collect::<Vec<String>>()
.join(", "),
)
})
.collect();
/// Configure List Menu
configure_list_menu_active: bool,
}
ScrollArea::vertical()
.id_source("relay_coverage")
.override_scroll_delta(Vec2 {
x: 0.0,
y: app.current_scroll_offset,
})
.show(ui, |ui| {
ui.push_id("general_feed_relays", |ui| {
TableBuilder::new(ui)
.striped(true)
.column(Column::auto_with_initial_suggestion(250.0).resizable(true))
.column(Column::auto().resizable(true))
.column(Column::auto().resizable(true))
.column(Column::auto().resizable(true))
.header(20.0, |mut header| {
header.col(|ui| {
ui.heading("Relay URL");
});
header.col(|ui| {
ui.heading("# Keys");
});
header.col(|ui| {
ui.heading("Reasons")
.on_hover_text("Reasons in [brackets] are persistent based on your relay usage configurations; if the connection drops, it will be restarted and resubscribed after a delay.");
});
header.col(|_| {});
})
.body(|body| {
body.rows(24.0, connected_relays.len(), |row_index, mut row| {
let relay_url = &connected_relays[row_index].0;
let reasons = &connected_relays[row_index].1;
row.col(|ui| {
crate::ui::widgets::break_anywhere_label(ui, &relay_url.0);
});
row.col(|ui| {
if let Some(ref assignment) =
GLOBALS.relay_picker.get_relay_assignment(relay_url)
{
ui.label(format!("{}", assignment.pubkeys.len()));
}
});
row.col(|ui| {
ui.label(reasons);
});
row.col(|ui| {
if ui.button("Disconnect").clicked() {
let _ = GLOBALS.to_overlord.send(
ToOverlordMessage::DropRelay(relay_url.to_owned()),
);
}
});
});
});
});
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
if ui.button("Pick Again").clicked() {
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PickRelays);
}
ui.add_space(12.0);
ui.heading("Coverage");
if GLOBALS.relay_picker.pubkey_counts_iter().count() > 0 {
for elem in GLOBALS.relay_picker.pubkey_counts_iter() {
let pk = elem.key();
let count = elem.value();
let name = GossipUi::display_name_from_pubkey_lookup(pk);
ui.label(format!("{}: coverage short by {} relay(s)", name, count));
}
} else {
ui.label("All followed people are fully covered.".to_owned());
}
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
ui.heading("Penalty Box");
ui.add_space(10.0);
let now = Unixtime::now().unwrap().0;
let excluded: Vec<(String, i64)> = GLOBALS.relay_picker.excluded_relays_iter().map(|refmulti| {
(refmulti.key().as_str().to_owned(),
*refmulti.value() - now)
}).collect();
TableBuilder::new(ui)
.striped(true)
.column(Column::auto().resizable(true))
.column(Column::auto().resizable(true))
.header(20.0, |mut header| {
header.col(|ui| {
ui.heading("Relay URL");
});
header.col(|ui| {
ui.heading("Time Remaining");
});
})
.body(|body| {
body.rows(24.0, excluded.len(), |row_index, mut row| {
let data = &excluded[row_index];
row.col(|ui| {
ui.label(&data.0);
});
row.col(|ui| {
ui.label(format!("{}", data.1));
});
});
});
});
} else if app.page == Page::RelaysAll {
all::update(app, ctx, frame, ui);
impl RelayUi {
pub(super) fn new() -> Self {
Self {
search: String::new(),
sort: RelaySorting::default(),
filter: RelayFilter::default(),
show_hidden: false,
show_details: false,
edit: None,
edit_relays: Vec::new(),
edit_done: None,
edit_needs_scroll: false,
add_dialog_step: AddRelayDialogStep::Inactive,
new_relay_url: "".to_string(),
configure_list_menu_active: false,
}
}
}
#[derive(PartialEq, Default)]
pub(super) enum RelaySorting {
#[default]
Rank,
Name,
WriteRelays,
AdvertiseRelays,
HighestFollowing,
HighestSuccessRate,
LowestSuccessRate,
}
impl RelaySorting {
pub fn get_name(&self) -> &str {
match self {
RelaySorting::Rank => "Rank",
RelaySorting::Name => "Name",
RelaySorting::WriteRelays => "Write Relays",
RelaySorting::AdvertiseRelays => "Advertise Relays",
RelaySorting::HighestFollowing => "Following",
RelaySorting::HighestSuccessRate => "Success Rate",
RelaySorting::LowestSuccessRate => "Failure Rate",
}
}
}
#[derive(PartialEq, Default)]
pub(super) enum RelayFilter {
#[default]
All,
Write,
Read,
Advertise,
Private,
}
impl RelayFilter {
pub fn get_name(&self) -> &str {
match self {
RelayFilter::All => "All",
RelayFilter::Write => "Write",
RelayFilter::Read => "Read",
RelayFilter::Advertise => "Advertise",
RelayFilter::Private => "Private",
}
}
}
#[derive(PartialEq, Default)]
enum AddRelayDialogStep {
#[default]
Inactive,
Step1UrlEntry,
Step2AwaitOverlord, // TODO add a configure step once we have overlord connection checking
}
///
/// Show the Relays UI
///
pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Frame, ui: &mut Ui) {
if app.page == Page::RelaysActivityMonitor {
active::update(app, ctx, frame, ui);
} else if app.page == Page::RelaysMine {
mine::update(app, ctx, frame, ui);
} else if app.page == Page::RelaysKnownNetwork {
known::update(app, ctx, frame, ui);
}
}
pub(super) fn relay_scroll_list(
app: &mut GossipUi,
ui: &mut Ui,
relays: Vec<Relay>,
id_source: Id,
) {
let scroll_size = ui.available_size_before_wrap();
let is_editing = app.relays.edit.is_some();
let enable_scroll = !is_editing && !egui::ScrollArea::is_scrolling(ui, id_source);
egui::ScrollArea::vertical()
.id_source(id_source)
.enable_scrolling(enable_scroll)
.show(ui, |ui| {
let mut pos_last_entry = ui.cursor().left_top();
let mut has_edit_target = false;
for db_relay in relays {
let db_url = db_relay.url.clone();
// is THIS entry being edited?
let edit = if let Some(edit_url) = &app.relays.edit {
if edit_url == &db_url {
has_edit_target = true;
true
} else {
false
}
} else {
false
};
// retrieve an updated copy of this relay when editing
let db_relay = if has_edit_target {
if let Ok(Some(entry)) = GLOBALS.storage.read_relay(&db_url) {
entry.clone() // update
} else {
db_relay // can't update
}
} else {
db_relay // don't update
};
// get details on this relay
let (is_connected, reasons) =
if let Some(entry) = GLOBALS.connected_relays.get(&db_url) {
(
true,
entry
.iter()
.map(|rj| {
if rj.persistent {
format!("[{}]", rj.reason)
} else {
rj.reason.to_string()
}
})
.collect::<Vec<String>>()
.join(", "),
)
} else {
(false, "".into())
};
// get timeout if any
let timeout_until = GLOBALS
.relay_picker
.excluded_relays_iter()
.find(|p| p.key() == &db_url)
.map(|f| *f.value());
let enabled = edit || !is_editing;
let mut widget = super::widgets::RelayEntry::new(db_relay, app);
widget.set_edit(edit);
widget.set_detail(app.relays.show_details);
widget.set_enabled(enabled);
widget.set_connected(is_connected);
widget.set_timeout(timeout_until);
widget.set_reasons(reasons);
if let Some(ref assignment) = GLOBALS.relay_picker.get_relay_assignment(&db_url) {
widget.set_user_count(assignment.pubkeys.len());
}
let response = ui.add_enabled(enabled, widget.clone());
if response.clicked() {
if !edit {
app.relays.edit = Some(db_url);
app.relays.edit_needs_scroll = true;
has_edit_target = true;
} else {
app.relays.edit_done = Some(db_url);
app.relays.edit = None;
}
} else {
if edit && has_edit_target && app.relays.edit_needs_scroll {
// on the start of an edit, scroll to the entry (after fixed sorting)
response.scroll_to_me(Some(egui::Align::Center));
app.relays.edit_needs_scroll = false;
} else if Some(db_url) == app.relays.edit_done {
// on the end of an edit, scroll to the entry (after sorting has reverted)
response.scroll_to_me(Some(egui::Align::Center));
app.relays.edit_done = None;
}
}
pos_last_entry = response.rect.left_top();
}
if !has_edit_target && !is_entry_dialog_active(app) {
// the relay we wanted to edit was not in the list anymore
// -> release edit modal
app.relays.edit = None;
}
// add enough space to show the last relay entry at the top when editing
if app.relays.edit.is_some() {
let desired_size = scroll_size - vec2(0.0, ui.cursor().top() - pos_last_entry.y);
ui.allocate_exact_size(desired_size, egui::Sense::hover());
}
});
}
pub(super) fn is_entry_dialog_active(app: &GossipUi) -> bool {
app.relays.add_dialog_step != AddRelayDialogStep::Inactive
}
pub(super) fn start_entry_dialog(app: &mut GossipUi) {
app.relays.add_dialog_step = AddRelayDialogStep::Step1UrlEntry;
}
pub(super) fn stop_entry_dialog(app: &mut GossipUi) {
app.relays.new_relay_url = "".to_string();
app.relays.add_dialog_step = AddRelayDialogStep::Inactive;
}
pub(super) fn entry_dialog(ctx: &Context, app: &mut GossipUi) {
let dlg_size = vec2(ctx.screen_rect().width() * 0.66, 120.0);
egui::Area::new("hide-background-area")
.fixed_pos(ctx.screen_rect().left_top())
.movable(false)
.interactable(false)
.order(egui::Order::Middle)
.show(ctx, |ui| {
ui.painter().rect_filled(
ctx.screen_rect(),
egui::Rounding::same(0.0),
egui::Color32::from_rgba_unmultiplied(0x9f, 0x9f, 0x9f, 102),
);
});
let id: Id = "relays-add-dialog".into();
let mut frame = egui::Frame::popup(&ctx.style());
let area = egui::Area::new(id)
.movable(false)
.interactable(true)
.order(egui::Order::Foreground)
.fixed_pos(ctx.screen_rect().center() - vec2(dlg_size.x / 2.0, dlg_size.y));
area.show_open_close_animation(
ctx,
&frame,
app.relays.add_dialog_step != AddRelayDialogStep::Inactive,
);
area.show(ctx, |ui| {
frame.fill = ui.visuals().extreme_bg_color;
frame.inner_margin = egui::Margin::symmetric(20.0, 10.0);
frame.show(ui, |ui| {
ui.set_min_size(dlg_size);
ui.set_max_size(dlg_size);
// ui.max_rect is inner_margin size
let tr = ui.max_rect().right_top();
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.heading("Add a new relay");
let rect = Rect::from_x_y_ranges(tr.x..=tr.x + 10.0, tr.y - 20.0..=tr.y - 10.0);
ui.allocate_ui_at_rect(rect, |ui| {
if ui
.add_sized(rect.size(), super::widgets::NavItem::new("\u{274C}", false))
.clicked()
{
stop_entry_dialog(app);
}
});
});
match app.relays.add_dialog_step {
AddRelayDialogStep::Inactive => {}
AddRelayDialogStep::Step1UrlEntry => entry_dialog_step1(ui, app),
AddRelayDialogStep::Step2AwaitOverlord => entry_dialog_step2(ui, app),
}
});
});
});
}
fn entry_dialog_step1(ui: &mut Ui, app: &mut GossipUi) {
ui.add_space(10.0);
ui.add(egui::Label::new("Enter relay URL:"));
ui.add_space(10.0);
// validate relay url (we are validating one UI frame later, shouldn't be an issue)
let is_url_valid = RelayUrl::try_from_str(&app.relays.new_relay_url).is_ok();
let edit_response = ui.horizontal(|ui| {
ui.style_mut().visuals.widgets.inactive.bg_stroke.width = 1.0;
ui.style_mut().visuals.widgets.hovered.bg_stroke.width = 1.0;
// change frame color to error when url is invalid
if !is_url_valid {
ui.style_mut().visuals.widgets.inactive.bg_stroke.color =
ui.style().visuals.error_fg_color;
ui.style_mut().visuals.selection.stroke.color = ui.style().visuals.error_fg_color;
}
ui.add(
text_edit_line!(app, app.relays.new_relay_url)
.desired_width(ui.available_width())
.hint_text("wss://myrelay.com"),
)
});
ui.add_space(10.0);
ui.allocate_ui_with_layout(
vec2(edit_response.inner.rect.width(), 30.0),
egui::Layout::left_to_right(egui::Align::Min),
|ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
ui.visuals_mut().widgets.inactive.weak_bg_fill = app.settings.theme.accent_color();
ui.visuals_mut().widgets.hovered.weak_bg_fill = {
let mut hsva: egui::ecolor::HsvaGamma =
app.settings.theme.accent_color().into();
hsva.v *= 0.8;
hsva.into()
};
ui.spacing_mut().button_padding *= 2.0;
let text = RichText::new("Check").color(ui.visuals().extreme_bg_color);
if ui
.add_enabled(is_url_valid, egui::Button::new(text))
.on_hover_cursor(egui::CursorIcon::PointingHand)
.clicked()
{
if let Ok(url) = RelayUrl::try_from_str(&app.relays.new_relay_url) {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::AddRelay(url.clone()));
GLOBALS.status_queue.write().write(format!(
"I asked the overlord to add relay {}. Check for it below.",
&app.relays.new_relay_url
));
// send user to known relays page (where the new entry should show up)
app.set_page(Page::RelaysKnownNetwork);
// search for the new relay so it shows at the top
app.relays.search = url.to_string();
// set the new relay to edit mode
app.relays.edit = Some(url);
app.relays.edit_needs_scroll = true;
// reset the filters so it will show
app.relays.filter = RelayFilter::All;
// go to next step
app.relays.add_dialog_step = AddRelayDialogStep::Step2AwaitOverlord;
app.relays.new_relay_url = "".to_owned();
} else {
GLOBALS
.status_queue
.write()
.write("That's not a valid relay URL.".to_owned());
}
}
});
},
);
}
fn entry_dialog_step2(ui: &mut Ui, app: &mut GossipUi) {
// the new relay has been set as the edit relay
if let Some(url) = app.relays.edit.clone() {
ui.add_space(10.0);
ui.add(egui::Label::new(
"Relay added and is ready to be configured.",
));
ui.add_space(10.0);
// if the overlord has added the relay, we are done for now
if GLOBALS.storage.read_relay(&url).is_ok() {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
ui.visuals_mut().widgets.inactive.weak_bg_fill = app.settings.theme.accent_color();
ui.visuals_mut().widgets.hovered.weak_bg_fill = {
let mut hsva: egui::ecolor::HsvaGamma =
app.settings.theme.accent_color().into();
hsva.v *= 0.8;
hsva.into()
};
ui.spacing_mut().button_padding *= 2.0;
let text = RichText::new("Configure").color(ui.visuals().extreme_bg_color);
if ui
.add(egui::Button::new(text))
.on_hover_cursor(egui::CursorIcon::PointingHand)
.clicked()
{
stop_entry_dialog(app);
}
});
}
} else {
ui.add_space(10.0);
ui.add(egui::Label::new("Adding relay..."));
ui.add_space(10.0);
ui.label("If this takes too long, something went wrong.");
ui.label("Use the 'X' to close this dialog and abort.");
}
}
///
/// 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)
.on_hover_text("Configure List View");
let btn_rect = response.rect;
let color = if response.hovered() {
app.settings.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));
if response.clicked() {
app.relays.configure_list_menu_active ^= true;
}
let mut seen_on_popup_position = response.rect.center_bottom();
seen_on_popup_position.x -= 150.0;
seen_on_popup_position.y += 18.0; // drop below the icon itself
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 = ui.visuals().extreme_bg_color;
frame.inner_margin = egui::Margin::symmetric(20.0, 10.0);
frame.show(ui, |ui| {
let size = ui.spacing().interact_size.y * egui::vec2(1.6, 0.8);
crate::ui::components::switch_with_size(ui, &mut app.relays.show_details, size);
ui.label("Show details");
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;
}
}
}
///
/// Draw relay sort comboBox
///
pub(super) fn relay_sort_combo(app: &mut GossipUi, ui: &mut Ui) {
let sort_combo = egui::ComboBox::from_id_source(Id::from("RelaySortCombo"));
sort_combo
.width(130.0)
.selected_text("Sort by ".to_string() + app.relays.sort.get_name())
.show_ui(ui, |ui| {
ui.selectable_value(
&mut app.relays.sort,
RelaySorting::Rank,
RelaySorting::Rank.get_name(),
);
ui.selectable_value(
&mut app.relays.sort,
RelaySorting::Name,
RelaySorting::Name.get_name(),
);
ui.selectable_value(
&mut app.relays.sort,
RelaySorting::HighestFollowing,
RelaySorting::HighestFollowing.get_name(),
);
ui.selectable_value(
&mut app.relays.sort,
RelaySorting::HighestSuccessRate,
RelaySorting::HighestSuccessRate.get_name(),
);
ui.selectable_value(
&mut app.relays.sort,
RelaySorting::LowestSuccessRate,
RelaySorting::LowestSuccessRate.get_name(),
);
ui.selectable_value(
&mut app.relays.sort,
RelaySorting::WriteRelays,
RelaySorting::WriteRelays.get_name(),
);
ui.selectable_value(
&mut app.relays.sort,
RelaySorting::AdvertiseRelays,
RelaySorting::AdvertiseRelays.get_name(),
);
});
}
///
/// Draw relay filter comboBox
///
pub(super) fn relay_filter_combo(app: &mut GossipUi, ui: &mut Ui) {
let filter_combo = egui::ComboBox::from_id_source(Id::from("RelayFilterCombo"));
filter_combo
.selected_text(app.relays.filter.get_name())
.show_ui(ui, |ui| {
ui.selectable_value(
&mut app.relays.filter,
RelayFilter::All,
RelayFilter::All.get_name(),
);
ui.selectable_value(
&mut app.relays.filter,
RelayFilter::Write,
RelayFilter::Write.get_name(),
);
ui.selectable_value(
&mut app.relays.filter,
RelayFilter::Read,
RelayFilter::Read.get_name(),
);
ui.selectable_value(
&mut app.relays.filter,
RelayFilter::Advertise,
RelayFilter::Advertise.get_name(),
);
ui.selectable_value(
&mut app.relays.filter,
RelayFilter::Private,
RelayFilter::Private.get_name(),
);
});
}
///
/// Filter a relay entry
/// - return: true if selected
///
pub(super) fn sort_relay(rui: &RelayUi, a: &Relay, b: &Relay) -> Ordering {
match rui.sort {
RelaySorting::Rank => b
.rank
.cmp(&a.rank)
.then(b.usage_bits.cmp(&a.usage_bits))
.then(a.url.cmp(&b.url)),
RelaySorting::Name => a.url.cmp(&b.url),
RelaySorting::WriteRelays => b
.has_usage_bits(Relay::WRITE)
.cmp(&a.has_usage_bits(Relay::WRITE))
.then(a.url.cmp(&b.url)),
RelaySorting::AdvertiseRelays => b
.has_usage_bits(Relay::ADVERTISE)
.cmp(&a.has_usage_bits(Relay::ADVERTISE))
.then(a.url.cmp(&b.url)),
RelaySorting::HighestFollowing => a.url.cmp(&b.url), // FIXME need following numbers here
RelaySorting::HighestSuccessRate => b
.success_rate()
.total_cmp(&a.success_rate())
.then(a.url.cmp(&b.url)),
RelaySorting::LowestSuccessRate => a
.success_rate()
.total_cmp(&b.success_rate())
.then(a.url.cmp(&b.url)),
}
}
///
/// Filter a relay entry
/// - return: true if selected
///
pub(super) fn filter_relay(rui: &RelayUi, ri: &Relay) -> bool {
let search = if rui.search.len() > 1 {
ri.url
.as_str()
.to_lowercase()
.contains(&rui.search.to_lowercase())
} else {
true
};
let filter = match rui.filter {
RelayFilter::All => true,
RelayFilter::Write => ri.has_usage_bits(Relay::WRITE),
RelayFilter::Read => ri.has_usage_bits(Relay::READ),
RelayFilter::Advertise => ri.has_usage_bits(Relay::ADVERTISE),
RelayFilter::Private => !ri.has_usage_bits(Relay::INBOX | Relay::OUTBOX),
};
search && filter
}

View File

@ -30,7 +30,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
ui.horizontal(|ui| {
ui.label("Manage individual relays on the");
if ui.link("Relays > Configure").clicked() {
app.set_page(Page::RelaysAll);
app.set_page(Page::RelaysKnownNetwork);
}
ui.label("page.");
});

View File

@ -16,8 +16,9 @@ impl ThemeDef for DefaultTheme {
}
fn accent_color(dark_mode: bool) -> Color32 {
#[allow(clippy::if_same_then_else)]
if dark_mode {
Color32::from_rgb(27, 31, 51)
Color32::from_rgb(85, 122, 149)
} else {
Color32::from_rgb(85, 122, 149)
}
@ -48,7 +49,7 @@ impl ThemeDef for DefaultTheme {
// pub window_margin: Margin,
// /// Button size is text size plus this on each side
// pub button_padding: Vec2,
// style.spacing.button_padding = vec2(10.0, 2.0);
// /// Horizontal and vertical margins within a menu frame.
// pub menu_margin: Margin,
@ -152,8 +153,8 @@ impl ThemeDef for DefaultTheme {
},
// Background colors
window_fill: Color32::from_gray(36), // pulldown menus and tooltips
panel_fill: Color32::from_gray(6), // panel backgrounds, even-table-rows
window_fill: Color32::from_gray(0x1F), // pulldown menus and tooltips
panel_fill: Color32::from_gray(0x18), // panel backgrounds, even-table-rows
faint_bg_color: Color32::from_gray(24), // odd-table-rows
extreme_bg_color: Color32::from_gray(45), // text input background; scrollbar background
code_bg_color: Color32::from_gray(64), // ???
@ -163,11 +164,7 @@ impl ThemeDef for DefaultTheme {
override_text_color: None,
warn_fg_color: Self::accent_complementary_color(true),
error_fg_color: Self::accent_complementary_color(true),
hyperlink_color: {
let mut hsva: ecolor::HsvaGamma = Self::accent_color(true).into();
hsva.v = (hsva.v + 0.5).min(1.0); // lighten
hsva.into()
},
hyperlink_color: Self::accent_color(true),
selection: Selection {
bg_fill: Color32::from_gray(40),
@ -423,7 +420,10 @@ impl ThemeDef for DefaultTheme {
}
fn navigation_bg_fill(dark_mode: bool) -> eframe::egui::Color32 {
Self::accent_color(dark_mode)
let mut hsva: ecolor::HsvaGamma = Self::accent_color(dark_mode).into();
hsva.s *= 0.7;
hsva.v = if dark_mode { 0.23 } else { 0.56 };
hsva.into()
}
fn navigation_text_color(dark_mode: bool) -> eframe::egui::Color32 {

View File

@ -67,6 +67,24 @@ macro_rules! theme_dispatch {
self.variant.name()
}
pub fn accent_color(&self) -> Color32 {
match self.variant {
$( $variant => $class::accent_color(self.dark_mode), )+
}
}
pub fn highlight_color(&self) -> Color32 {
match self.variant {
$( $variant => $class::highlight_color(self.dark_mode), )+
}
}
pub fn accent_complementary_color(&self) -> Color32 {
match self.variant {
$( $variant => $class::accent_complementary_color(self.dark_mode), )+
}
}
pub fn get_style(&self) -> Style {
match self.variant {
$( $variant => $class::get_style(self.dark_mode), )+

View File

@ -1,19 +1,27 @@
mod copy_button;
pub use copy_button::CopyButton;
use eframe::egui::{FontSelection, Ui, WidgetText};
mod nav_item;
pub use nav_item::NavItem;
pub fn break_anywhere_label(ui: &mut Ui, text: impl Into<WidgetText>) {
let mut job = text.into().into_text_job(
ui.style(),
FontSelection::Default,
ui.layout().vertical_align(),
);
job.job.sections.first_mut().unwrap().format.color =
ui.style().visuals.widgets.noninteractive.fg_stroke.color;
job.job.wrap.break_anywhere = true;
ui.label(job.job);
}
mod relay_entry;
pub use relay_entry::{RelayEntry, RelayEntryView};
use eframe::egui::widgets::TextEdit;
use eframe::egui::{FontSelection, Ui, WidgetText};
use egui_winit::egui::{vec2, Rect, Response, Sense};
// pub fn break_anywhere_label(ui: &mut Ui, text: impl Into<WidgetText>) {
// let mut job = text.into().into_text_job(
// ui.style(),
// FontSelection::Default,
// ui.layout().vertical_align(),
// );
// job.job.sections.first_mut().unwrap().format.color =
// ui.style().visuals.widgets.noninteractive.fg_stroke.color;
// job.job.wrap.break_anywhere = true;
// ui.label(job.job);
// }
pub fn break_anywhere_hyperlink_to(ui: &mut Ui, text: impl Into<WidgetText>, url: impl ToString) {
let mut job = text.into().into_text_job(
@ -24,3 +32,33 @@ pub fn break_anywhere_hyperlink_to(ui: &mut Ui, text: impl Into<WidgetText>, url
job.job.wrap.break_anywhere = true;
ui.hyperlink_to(job.job, url);
}
pub fn search_filter_field(ui: &mut Ui, field: &mut String, width: f32) -> Response {
// search field
let response = ui.add(
TextEdit::singleline(field)
.text_color(ui.visuals().widgets.inactive.fg_stroke.color)
.desired_width(width),
);
let rect = Rect::from_min_size(
response.rect.right_top() - vec2(response.rect.height(), 0.0),
vec2(response.rect.height(), response.rect.height()),
);
// search clear button
if ui
.put(
rect,
NavItem::new("\u{2715}", field.is_empty())
.color(ui.visuals().widgets.inactive.fg_stroke.color)
.active_color(ui.visuals().widgets.active.fg_stroke.color)
.hover_color(ui.visuals().hyperlink_color)
.sense(Sense::click()),
)
.clicked()
{
field.clear();
}
response
}

View File

@ -77,14 +77,7 @@ impl NavItem {
impl NavItem {
/// Do layout and position the galley in the ui, without painting it or adding widget info.
pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, WidgetTextGalley, Response) {
let sense = self.sense.unwrap_or_else(|| {
// We only want to focus labels if the screen reader is on.
if ui.memory(|mem| mem.options.screen_reader) {
Sense::focusable_noninteractive()
} else {
Sense::hover()
}
});
let sense = self.sense.unwrap_or(Sense::click());
if let WidgetText::Galley(galley) = self.text {
// If the user said "use this specific galley", then just use it:
let (rect, response) = ui.allocate_exact_size(galley.size(), sense);
@ -196,6 +189,10 @@ impl Widget for NavItem {
Some(ui.style().interact(&response).text_color())
};
response
.clone()
.on_hover_cursor(egui::CursorIcon::PointingHand);
ui.painter().add(epaint::TextShape {
pos,
galley: text_galley.galley,

File diff suppressed because it is too large Load Diff

View File

@ -129,7 +129,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
ui.horizontal(|ui| {
ui.label("You need to");
if ui.link("configure write relays").clicked() {
app.set_page(Page::RelaysAll);
app.set_page(Page::RelaysKnownNetwork);
}
ui.label("to edit/save metadata.");
});