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 }