diff --git a/assets/icons/connected_icon_4x.png b/assets/icons/connected_icon_4x.png new file mode 100644 index 0000000..9a84d1d Binary files /dev/null and b/assets/icons/connected_icon_4x.png differ diff --git a/assets/icons/connecting_icon_4x.png b/assets/icons/connecting_icon_4x.png new file mode 100644 index 0000000..aed616c Binary files /dev/null and b/assets/icons/connecting_icon_4x.png differ diff --git a/assets/icons/delete_icon_4x.png b/assets/icons/delete_icon_4x.png new file mode 100644 index 0000000..d7c5afa Binary files /dev/null and b/assets/icons/delete_icon_4x.png differ diff --git a/assets/icons/disconnected_icon_4x.png b/assets/icons/disconnected_icon_4x.png new file mode 100644 index 0000000..7dc34f8 Binary files /dev/null and b/assets/icons/disconnected_icon_4x.png differ diff --git a/src/lib.rs b/src/lib.rs index 82c66e1..1cf6785 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,8 @@ mod key_parsing; pub mod login_manager; mod notecache; mod profile; +pub mod relay_pool_manager; +pub mod relay_view; mod result; mod time; mod timecache; diff --git a/src/relay_pool_manager.rs b/src/relay_pool_manager.rs new file mode 100644 index 0000000..9550a36 --- /dev/null +++ b/src/relay_pool_manager.rs @@ -0,0 +1,54 @@ +use enostr::RelayPool; +pub use enostr::RelayStatus; + +/// The interface to a RelayPool for UI components. +/// Represents all user-facing operations that can be performed for a user's relays +pub struct RelayPoolManager<'a> { + pub pool: &'a mut RelayPool, +} + +pub struct RelayInfo<'a> { + pub relay_url: &'a str, + pub status: &'a RelayStatus, +} + +impl<'a> RelayPoolManager<'a> { + pub fn new(pool: &'a mut RelayPool) -> Self { + RelayPoolManager { pool } + } + + pub fn get_relay_infos(&self) -> Vec { + self.pool + .relays + .iter() + .map(|relay| RelayInfo { + relay_url: &relay.relay.url, + status: &relay.relay.status, + }) + .collect() + } + + /// index of the Vec from get_relay_infos + pub fn remove_relay(&mut self, index: usize) { + if index < self.pool.relays.len() { + self.pool.relays.remove(index); + } + } + + /// removes all specified relay indicies shown in get_relay_infos + pub fn remove_relays(&mut self, mut indices: Vec) { + indices.sort_unstable_by(|a, b| b.cmp(a)); + indices.iter().for_each(|index| self.remove_relay(*index)); + } + + pub fn add_relay(&mut self, ctx: &egui::Context, relay_url: String) { + let _ = self.pool.add_url(relay_url, create_wakeup(ctx)); + } +} + +fn create_wakeup(ctx: &egui::Context) -> impl Fn() + Send + Sync + Clone + 'static { + let ctx = ctx.clone(); + move || { + ctx.request_repaint(); + } +} diff --git a/src/relay_view.rs b/src/relay_view.rs new file mode 100644 index 0000000..d7a1fe3 --- /dev/null +++ b/src/relay_view.rs @@ -0,0 +1,171 @@ +use crate::relay_pool_manager::{RelayPoolManager, RelayStatus}; +use egui::{Align, Button, Frame, Layout, Margin, Rgba, RichText, Rounding, Ui, Vec2}; + +use crate::app_style::NotedeckTextStyle; + +pub struct RelayView<'a> { + ctx: &'a egui::Context, + manager: RelayPoolManager<'a>, +} + +impl<'a> RelayView<'a> { + pub fn new(ctx: &'a egui::Context, manager: RelayPoolManager<'a>) -> Self { + RelayView { ctx, manager } + } + + pub fn panel(&'a mut self) { + let mut indices_to_remove: Option> = None; + + egui::CentralPanel::default().show(self.ctx, |ui| { + ui.add_space(24.0); + + ui.horizontal(|ui| { + ui.with_layout(Layout::left_to_right(Align::Center), |ui| { + ui.label( + RichText::new("Relays") + .text_style(NotedeckTextStyle::Heading2.text_style()), + ); + }); + + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui.add(add_relay_button()).clicked() { + // TODO: navigate to 'add relay view' + }; + }); + }); + + ui.add_space(8.0); + + egui::ScrollArea::vertical() + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .show(ui, |ui| { + indices_to_remove = self.show_relays(ui); + }); + }); + + if let Some(indices) = indices_to_remove { + self.manager.remove_relays(indices); + } + } + + /// Show the current relays, and returns the indices of relays the user requested to delete + fn show_relays(&'a self, ui: &mut Ui) -> Option> { + let mut indices_to_remove: Option> = None; + for (index, relay_info) in self.manager.get_relay_infos().iter().enumerate() { + ui.add_space(8.0); + ui.vertical_centered_justified(|ui| { + relay_frame(ui).show(ui, |ui| { + ui.horizontal(|ui| { + ui.with_layout(Layout::left_to_right(Align::Center), |ui| { + Frame::none() + // This frame is needed to add margin because the label will be added to the outer frame first and centered vertically before the connection status is added so the vertical centering isn't accurate. + // TODO: remove this hack and actually center the url & status at the same time + .inner_margin(Margin::symmetric(0.0, 4.0)) + .show(ui, |ui| { + egui::ScrollArea::horizontal() + .id_source(index) + .max_width( + ui.max_rect().width() + - get_right_side_width(relay_info.status), + ) // TODO: refactor to dynamically check the size of the 'right to left' portion and set the max width to be the screen width minus padding minus 'right to left' width + .show(ui, |ui| { + ui.label( + RichText::new(relay_info.relay_url) + .text_style( + NotedeckTextStyle::Monospace.text_style(), + ) + .color( + ui.style() + .visuals + .noninteractive() + .fg_stroke + .color, + ), + ); + }); + }); + }); + + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui.add(delete_button(ui.visuals().dark_mode)).clicked() { + indices_to_remove.get_or_insert_with(Vec::new).push(index); + }; + + show_connection_status(ui, relay_info.status); + }); + }); + }); + }); + } + + indices_to_remove + } +} + +fn get_right_side_width(status: &RelayStatus) -> f32 { + match status { + RelayStatus::Connected => 150.0, + RelayStatus::Connecting => 160.0, + RelayStatus::Disconnected => 175.0, + } +} + +fn add_relay_button() -> egui::Button<'static> { + Button::new("+ Add relay").min_size(Vec2::new(0.0, 32.0)) +} + +fn delete_button(dark_mode: bool) -> egui::Button<'static> { + let img_data = if dark_mode { + egui::include_image!("../assets/icons/delete_icon_4x.png") + } else { + // TODO: use light delete icon + egui::include_image!("../assets/icons/delete_icon_4x.png") + }; + + egui::Button::image(egui::Image::new(img_data).max_width(10.0)).frame(false) +} + +fn relay_frame(ui: &mut Ui) -> Frame { + Frame::none() + .inner_margin(Margin::same(8.0)) + .rounding(ui.style().noninteractive().rounding) + .stroke(ui.style().visuals.noninteractive().bg_stroke) +} + +fn show_connection_status(ui: &mut Ui, status: &RelayStatus) { + let fg_color = match status { + RelayStatus::Connected => ui.visuals().selection.bg_fill, + RelayStatus::Connecting => ui.visuals().warn_fg_color, + RelayStatus::Disconnected => ui.visuals().error_fg_color, + }; + let bg_color = egui::lerp(Rgba::from(fg_color)..=Rgba::BLACK, 0.8).into(); + + let label_text = match status { + RelayStatus::Connected => "Connected", + RelayStatus::Connecting => "Connecting...", + RelayStatus::Disconnected => "Not Connected", + }; + + let frame = Frame::none() + .rounding(Rounding::same(100.0)) + .fill(bg_color) + .inner_margin(Margin::symmetric(12.0, 4.0)); + + frame.show(ui, |ui| { + ui.label(RichText::new(label_text).color(fg_color)); + ui.add(get_connection_icon(status)); + }); +} + +fn get_connection_icon(status: &RelayStatus) -> egui::Image<'static> { + let img_data = match status { + RelayStatus::Connected => egui::include_image!("../assets/icons/connected_icon_4x.png"), + RelayStatus::Connecting => egui::include_image!("../assets/icons/connecting_icon_4x.png"), + RelayStatus::Disconnected => { + egui::include_image!("../assets/icons/disconnected_icon_4x.png") + } + }; + + egui::Image::new(img_data) +}