notedeck

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

relay.rs (11850B)


      1 use std::collections::HashMap;
      2 
      3 use crate::ui::{Preview, PreviewConfig};
      4 use egui::{Align, Button, CornerRadius, Frame, Id, Layout, Margin, Rgba, RichText, Ui, Vec2};
      5 use enostr::{RelayPool, RelayStatus};
      6 use notedeck::{tr, Localization, NotedeckTextStyle, RelayAction};
      7 use notedeck_ui::app_images;
      8 use notedeck_ui::{colors::PINK, padding};
      9 use tracing::debug;
     10 
     11 use super::widgets::styled_button;
     12 
     13 pub struct RelayView<'a> {
     14     pool: &'a RelayPool,
     15     id_string_map: &'a mut HashMap<Id, String>,
     16     i18n: &'a mut Localization,
     17 }
     18 
     19 impl RelayView<'_> {
     20     pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<RelayAction> {
     21         let mut action = None;
     22         Frame::new()
     23             .inner_margin(Margin::symmetric(10, 0))
     24             .show(ui, |ui| {
     25                 ui.add_space(24.0);
     26 
     27                 ui.horizontal(|ui| {
     28                     ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
     29                         ui.label(
     30                             RichText::new(tr!(self.i18n, "Relays", "Label for relay list section"))
     31                                 .text_style(NotedeckTextStyle::Heading2.text_style()),
     32                         );
     33                     });
     34                 });
     35 
     36                 ui.add_space(8.0);
     37 
     38                 egui::ScrollArea::vertical()
     39                     .id_salt(RelayView::scroll_id())
     40                     .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
     41                     .auto_shrink([false; 2])
     42                     .show(ui, |ui| {
     43                         if let Some(relay_to_remove) = self.show_relays(ui) {
     44                             action = Some(RelayAction::Remove(relay_to_remove));
     45                         }
     46                         ui.add_space(8.0);
     47                         if let Some(relay_to_add) = self.show_add_relay_ui(ui) {
     48                             action = Some(RelayAction::Add(relay_to_add));
     49                         }
     50                     });
     51             });
     52 
     53         action
     54     }
     55 
     56     pub fn scroll_id() -> egui::Id {
     57         egui::Id::new("relay_scroll")
     58     }
     59 }
     60 
     61 impl<'a> RelayView<'a> {
     62     pub fn new(
     63         pool: &'a RelayPool,
     64         id_string_map: &'a mut HashMap<Id, String>,
     65         i18n: &'a mut Localization,
     66     ) -> Self {
     67         RelayView {
     68             pool,
     69             id_string_map,
     70             i18n,
     71         }
     72     }
     73 
     74     pub fn panel(&mut self, ui: &mut egui::Ui) {
     75         egui::CentralPanel::default().show(ui.ctx(), |ui| self.ui(ui));
     76     }
     77 
     78     /// Show the current relays and return a relay the user selected to delete
     79     fn show_relays(&mut self, ui: &mut Ui) -> Option<String> {
     80         let mut relay_to_remove = None;
     81         for (index, relay_info) in get_relay_infos(self.pool).iter().enumerate() {
     82             ui.add_space(8.0);
     83             ui.vertical_centered_justified(|ui| {
     84                 relay_frame(ui).show(ui, |ui| {
     85                     ui.horizontal(|ui| {
     86                         ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
     87                             Frame::new()
     88                                 // 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.
     89                                 // TODO: remove this hack and actually center the url & status at the same time
     90                                 .inner_margin(Margin::symmetric(0, 4))
     91                                 .show(ui, |ui| {
     92                                     egui::ScrollArea::horizontal()
     93                                         .id_salt(index)
     94                                         .max_width(
     95                                             ui.max_rect().width()
     96                                                 - get_right_side_width(relay_info.status),
     97                                         ) // 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
     98                                         .show(ui, |ui| {
     99                                             ui.label(
    100                                                 RichText::new(relay_info.relay_url)
    101                                                     .text_style(
    102                                                         NotedeckTextStyle::Monospace.text_style(),
    103                                                     )
    104                                                     .color(
    105                                                         ui.style()
    106                                                             .visuals
    107                                                             .noninteractive()
    108                                                             .fg_stroke
    109                                                             .color,
    110                                                     ),
    111                                             );
    112                                         });
    113                                 });
    114                         });
    115 
    116                         ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
    117                             if ui.add(delete_button(ui.visuals().dark_mode)).clicked() {
    118                                 relay_to_remove = Some(relay_info.relay_url.to_string());
    119                             };
    120 
    121                             show_connection_status(ui, self.i18n, relay_info.status);
    122                         });
    123                     });
    124                 });
    125             });
    126         }
    127         relay_to_remove
    128     }
    129 
    130     const RELAY_PREFILL: &'static str = "wss://";
    131 
    132     fn show_add_relay_ui(&mut self, ui: &mut Ui) -> Option<String> {
    133         let id = ui.id().with("add-relay)");
    134         match self.id_string_map.get(&id) {
    135             None => {
    136                 ui.with_layout(Layout::top_down(Align::Min), |ui| {
    137                     let relay_button = add_relay_button(self.i18n);
    138                     if ui.add(relay_button).clicked() {
    139                         debug!("add relay clicked");
    140                         self.id_string_map
    141                             .insert(id, Self::RELAY_PREFILL.to_string());
    142                     };
    143                 });
    144                 None
    145             }
    146             Some(_) => {
    147                 ui.with_layout(Layout::top_down(Align::Min), |ui| {
    148                     self.add_relay_entry(ui, id)
    149                 })
    150                 .inner
    151             }
    152         }
    153     }
    154 
    155     pub fn add_relay_entry(&mut self, ui: &mut Ui, id: Id) -> Option<String> {
    156         padding(16.0, ui, |ui| {
    157             let text_buffer = self
    158                 .id_string_map
    159                 .entry(id)
    160                 .or_insert_with(|| Self::RELAY_PREFILL.to_string());
    161             let is_enabled = self.pool.is_valid_url(text_buffer);
    162             let text_edit = egui::TextEdit::singleline(text_buffer)
    163                 .hint_text(
    164                     RichText::new(tr!(
    165                         self.i18n,
    166                         "Enter the relay here",
    167                         "Placeholder for relay input field"
    168                     ))
    169                     .text_style(NotedeckTextStyle::Body.text_style()),
    170                 )
    171                 .vertical_align(Align::Center)
    172                 .desired_width(f32::INFINITY)
    173                 .min_size(Vec2::new(0.0, 40.0))
    174                 .margin(Margin::same(12));
    175             ui.add(text_edit);
    176             ui.add_space(8.0);
    177             if ui
    178                 .add_sized(
    179                     egui::vec2(50.0, 40.0),
    180                     add_relay_button2(self.i18n, is_enabled),
    181                 )
    182                 .clicked()
    183             {
    184                 self.id_string_map.remove(&id) // remove and return the value
    185             } else {
    186                 None
    187             }
    188         })
    189         .inner
    190     }
    191 }
    192 
    193 fn add_relay_button(i18n: &mut Localization) -> Button<'static> {
    194     Button::image_and_text(
    195         app_images::add_relay_image().fit_to_exact_size(Vec2::new(48.0, 48.0)),
    196         RichText::new(tr!(i18n, "Add relay", "Button label to add a relay"))
    197             .size(16.0)
    198             // TODO: this color should not be hard coded. Find some way to add it to the visuals
    199             .color(PINK),
    200     )
    201     .frame(false)
    202 }
    203 
    204 fn add_relay_button2<'a>(i18n: &'a mut Localization, is_enabled: bool) -> impl egui::Widget + 'a {
    205     move |ui: &mut egui::Ui| -> egui::Response {
    206         let add_text = tr!(i18n, "Add", "Button label to add a relay");
    207         let button_widget = styled_button(add_text.as_str(), notedeck_ui::colors::PINK);
    208         ui.add_enabled(is_enabled, button_widget)
    209     }
    210 }
    211 
    212 fn get_right_side_width(status: RelayStatus) -> f32 {
    213     match status {
    214         RelayStatus::Connected => 150.0,
    215         RelayStatus::Connecting => 160.0,
    216         RelayStatus::Disconnected => 175.0,
    217     }
    218 }
    219 
    220 fn delete_button(dark_mode: bool) -> egui::Button<'static> {
    221     let img = if dark_mode {
    222         app_images::delete_dark_image()
    223     } else {
    224         app_images::delete_light_image()
    225     };
    226 
    227     egui::Button::image(img.max_width(10.0)).frame(false)
    228 }
    229 
    230 fn relay_frame(ui: &mut Ui) -> Frame {
    231     Frame::new()
    232         .inner_margin(Margin::same(8))
    233         .corner_radius(ui.style().noninteractive().corner_radius)
    234         .stroke(ui.style().visuals.noninteractive().bg_stroke)
    235 }
    236 
    237 fn show_connection_status(ui: &mut Ui, i18n: &mut Localization, status: RelayStatus) {
    238     let fg_color = match status {
    239         RelayStatus::Connected => ui.visuals().selection.bg_fill,
    240         RelayStatus::Connecting => ui.visuals().warn_fg_color,
    241         RelayStatus::Disconnected => ui.visuals().error_fg_color,
    242     };
    243     let bg_color = egui::lerp(Rgba::from(fg_color)..=Rgba::BLACK, 0.8).into();
    244 
    245     let label_text = match status {
    246         RelayStatus::Connected => tr!(i18n, "Connected", "Status label for connected relay"),
    247         RelayStatus::Connecting => tr!(i18n, "Connecting...", "Status label for connecting relay"),
    248         RelayStatus::Disconnected => {
    249             tr!(i18n, "Not Connected", "Status label for disconnected relay")
    250         }
    251     };
    252 
    253     let frame = Frame::new()
    254         .corner_radius(CornerRadius::same(100))
    255         .fill(bg_color)
    256         .inner_margin(Margin::symmetric(12, 4));
    257 
    258     frame.show(ui, |ui| {
    259         ui.label(RichText::new(label_text).color(fg_color));
    260         ui.add(get_connection_icon(status));
    261     });
    262 }
    263 
    264 fn get_connection_icon(status: RelayStatus) -> egui::Image<'static> {
    265     match status {
    266         RelayStatus::Connected => app_images::connected_image(),
    267         RelayStatus::Connecting => app_images::connecting_image(),
    268         RelayStatus::Disconnected => app_images::disconnected_image(),
    269     }
    270 }
    271 
    272 struct RelayInfo<'a> {
    273     pub relay_url: &'a str,
    274     pub status: RelayStatus,
    275 }
    276 
    277 fn get_relay_infos(pool: &RelayPool) -> Vec<RelayInfo> {
    278     pool.relays
    279         .iter()
    280         .map(|relay| RelayInfo {
    281             relay_url: relay.url(),
    282             status: relay.status(),
    283         })
    284         .collect()
    285 }
    286 
    287 // PREVIEWS
    288 
    289 mod preview {
    290     use super::*;
    291     use crate::test_data::sample_pool;
    292     use notedeck::{App, AppAction, AppContext};
    293 
    294     pub struct RelayViewPreview {
    295         pool: RelayPool,
    296     }
    297 
    298     impl RelayViewPreview {
    299         fn new() -> Self {
    300             RelayViewPreview {
    301                 pool: sample_pool(),
    302             }
    303         }
    304     }
    305 
    306     impl App for RelayViewPreview {
    307         fn update(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
    308             self.pool.try_recv();
    309             let mut id_string_map = HashMap::new();
    310             RelayView::new(app.pool, &mut id_string_map, app.i18n).ui(ui);
    311             None
    312         }
    313     }
    314 
    315     impl Preview for RelayView<'_> {
    316         type Prev = RelayViewPreview;
    317 
    318         fn preview(_cfg: PreviewConfig) -> Self::Prev {
    319             RelayViewPreview::new()
    320         }
    321     }
    322 }