mirror of
https://github.com/mikedilger/gossip.git
synced 2024-09-29 08:21:47 +00:00
Merge remote-tracking branch 'bushmann/feature/relay-list-widget' into unstable
This commit is contained in:
commit
1e4ecc888d
3
assets/option.svg
Normal file
3
assets/option.svg
Normal 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 |
@ -34,6 +34,7 @@ pub enum ToOverlordMessage {
|
||||
PushMetadata(Metadata),
|
||||
ReengageMinion(RelayUrl, Vec<RelayJob>),
|
||||
RefreshFollowedMetadata,
|
||||
ClearAllUsageOnRelay(RelayUrl),
|
||||
Repost(Id),
|
||||
RankRelay(RelayUrl, u8),
|
||||
SaveSettings,
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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.");
|
||||
});
|
||||
|
@ -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.");
|
||||
});
|
||||
|
105
src/ui/mod.rs
105
src/ui/mod.rs
@ -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
102
src/ui/relays/active.rs
Normal 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
|
||||
}
|
@ -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
85
src/ui/relays/known.rs
Normal 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
56
src/ui/relays/mine.rs
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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.");
|
||||
});
|
||||
|
@ -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 {
|
||||
|
@ -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), )+
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
1290
src/ui/widgets/relay_entry.rs
Normal file
1290
src/ui/widgets/relay_entry.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -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.");
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user