notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 349e3baa995199afe9211d28cbebae27bc4e911a
parent 805e18261c4889e4e4291ec3afb6143bee2b47f0
Author: kernelkind <kernelkind@gmail.com>
Date:   Wed, 17 Apr 2024 13:36:25 -0400

Add relay view

Signed-off-by: kernelkind <kernelkind@gmail.com>
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Aassets/icons/connected_icon_4x.png | 0
Aassets/icons/connecting_icon_4x.png | 0
Aassets/icons/delete_icon_4x.png | 0
Aassets/icons/disconnected_icon_4x.png | 0
Msrc/lib.rs | 2++
Asrc/relay_pool_manager.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/relay_view.rs | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 227 insertions(+), 0 deletions(-)

diff --git a/assets/icons/connected_icon_4x.png b/assets/icons/connected_icon_4x.png Binary files differ. diff --git a/assets/icons/connecting_icon_4x.png b/assets/icons/connecting_icon_4x.png Binary files differ. diff --git a/assets/icons/delete_icon_4x.png b/assets/icons/delete_icon_4x.png Binary files differ. diff --git a/assets/icons/disconnected_icon_4x.png b/assets/icons/disconnected_icon_4x.png Binary files differ. diff --git 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 @@ -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<RelayInfo> { + self.pool + .relays + .iter() + .map(|relay| RelayInfo { + relay_url: &relay.relay.url, + status: &relay.relay.status, + }) + .collect() + } + + /// index of the Vec<RelayInfo> 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<usize>) { + 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 @@ -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<Vec<usize>> = 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<Vec<usize>> { + let mut indices_to_remove: Option<Vec<usize>> = 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) +}