notedeck

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

relay.rs (11993B)


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