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 }