add_column.rs (14348B)
1 use core::f32; 2 use serde::{Deserialize, Serialize}; 3 use std::collections::HashMap; 4 5 use egui::{ 6 pos2, vec2, Align, Color32, FontId, Id, ImageSource, Margin, Pos2, Rect, RichText, Separator, 7 Ui, Vec2, 8 }; 9 use nostrdb::Ndb; 10 use tracing::error; 11 12 use crate::{ 13 app_style::{get_font_size, NotedeckTextStyle}, 14 login_manager::AcquireKeyState, 15 timeline::{PubkeySource, Timeline, TimelineKind}, 16 ui::anim::ICON_EXPANSION_MULTIPLE, 17 user_account::UserAccount, 18 Damus, 19 }; 20 21 use super::{anim::AnimationHelper, padding}; 22 23 pub enum AddColumnResponse { 24 Timeline(Timeline), 25 UndecidedNotification, 26 ExternalNotification, 27 } 28 29 pub enum NotificationColumnType { 30 Home, 31 External, 32 } 33 34 #[derive(Clone, Debug)] 35 enum AddColumnOption { 36 Universe, 37 UndecidedNotification, 38 ExternalNotification, 39 Notification(PubkeySource), 40 Home(PubkeySource), 41 } 42 43 #[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize)] 44 pub enum AddColumnRoute { 45 Base, 46 UndecidedNotification, 47 ExternalNotification, 48 } 49 50 impl AddColumnOption { 51 pub fn take_as_response( 52 self, 53 ndb: &Ndb, 54 cur_account: Option<&UserAccount>, 55 ) -> Option<AddColumnResponse> { 56 match self { 57 AddColumnOption::Universe => TimelineKind::Universe 58 .into_timeline(ndb, None) 59 .map(AddColumnResponse::Timeline), 60 AddColumnOption::Notification(pubkey) => TimelineKind::Notifications(pubkey) 61 .into_timeline(ndb, cur_account.map(|a| a.pubkey.bytes())) 62 .map(AddColumnResponse::Timeline), 63 AddColumnOption::UndecidedNotification => { 64 Some(AddColumnResponse::UndecidedNotification) 65 } 66 AddColumnOption::Home(pubkey) => { 67 let tlk = TimelineKind::contact_list(pubkey); 68 tlk.into_timeline(ndb, cur_account.map(|a| a.pubkey.bytes())) 69 .map(AddColumnResponse::Timeline) 70 } 71 AddColumnOption::ExternalNotification => Some(AddColumnResponse::ExternalNotification), 72 } 73 } 74 } 75 76 pub struct AddColumnView<'a> { 77 key_state_map: &'a mut HashMap<Id, AcquireKeyState>, 78 ndb: &'a Ndb, 79 cur_account: Option<&'a UserAccount>, 80 } 81 82 impl<'a> AddColumnView<'a> { 83 pub fn new( 84 key_state_map: &'a mut HashMap<Id, AcquireKeyState>, 85 ndb: &'a Ndb, 86 cur_account: Option<&'a UserAccount>, 87 ) -> Self { 88 Self { 89 key_state_map, 90 ndb, 91 cur_account, 92 } 93 } 94 95 pub fn ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> { 96 let mut selected_option: Option<AddColumnResponse> = None; 97 for column_option_data in self.get_base_options() { 98 let option = column_option_data.option.clone(); 99 if self.column_option_ui(ui, column_option_data).clicked() { 100 selected_option = option.take_as_response(self.ndb, self.cur_account); 101 } 102 103 ui.add(Separator::default().spacing(0.0)); 104 } 105 106 selected_option 107 } 108 109 fn notifications_ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> { 110 let mut selected_option: Option<AddColumnResponse> = None; 111 for column_option_data in self.get_notifications_options() { 112 let option = column_option_data.option.clone(); 113 if self.column_option_ui(ui, column_option_data).clicked() { 114 selected_option = option.take_as_response(self.ndb, self.cur_account); 115 } 116 117 ui.add(Separator::default().spacing(0.0)); 118 } 119 120 selected_option 121 } 122 123 fn external_notification_ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> { 124 padding(16.0, ui, |ui| { 125 let id = ui.id().with("external_notif"); 126 let key_state = self.key_state_map.entry(id).or_default(); 127 128 let text_edit = key_state.get_acquire_textedit(|text| { 129 egui::TextEdit::singleline(text) 130 .hint_text( 131 RichText::new("Enter the user's key (npub, hex, nip05) here...") 132 .text_style(NotedeckTextStyle::Body.text_style()), 133 ) 134 .vertical_align(Align::Center) 135 .desired_width(f32::INFINITY) 136 .min_size(Vec2::new(0.0, 40.0)) 137 .margin(Margin::same(12.0)) 138 }); 139 140 ui.add(text_edit); 141 142 if ui.button("Add").clicked() { 143 key_state.apply_acquire(); 144 } 145 146 if key_state.is_awaiting_network() { 147 ui.spinner(); 148 } 149 150 if let Some(error) = key_state.check_for_error() { 151 error!("acquire key error: {}", error); 152 ui.colored_label( 153 Color32::RED, 154 "Please enter a valid npub, public hex key or nip05", 155 ); 156 } 157 158 if let Some(keypair) = key_state.check_for_successful_login() { 159 key_state.should_create_new(); 160 AddColumnOption::Notification(PubkeySource::Explicit(keypair.pubkey)) 161 .take_as_response(self.ndb, self.cur_account) 162 } else { 163 None 164 } 165 }) 166 .inner 167 } 168 169 fn column_option_ui(&mut self, ui: &mut Ui, data: ColumnOptionData) -> egui::Response { 170 let icon_padding = 8.0; 171 let min_icon_width = 32.0; 172 let height_padding = 12.0; 173 let max_width = ui.available_width(); 174 let title_style = NotedeckTextStyle::Body; 175 let desc_style = NotedeckTextStyle::Button; 176 let title_min_font_size = get_font_size(ui.ctx(), &title_style); 177 let desc_min_font_size = get_font_size(ui.ctx(), &desc_style); 178 179 let max_height = { 180 let max_wrap_width = 181 max_width - ((icon_padding * 2.0) + (min_icon_width * ICON_EXPANSION_MULTIPLE)); 182 let title_max_font = FontId::new( 183 title_min_font_size * ICON_EXPANSION_MULTIPLE, 184 title_style.font_family(), 185 ); 186 let desc_max_font = FontId::new( 187 desc_min_font_size * ICON_EXPANSION_MULTIPLE, 188 desc_style.font_family(), 189 ); 190 let max_desc_galley = ui.fonts(|f| { 191 f.layout( 192 data.description.to_string(), 193 desc_max_font, 194 Color32::WHITE, 195 max_wrap_width, 196 ) 197 }); 198 199 let max_title_galley = ui.fonts(|f| { 200 f.layout( 201 data.title.to_string(), 202 title_max_font, 203 Color32::WHITE, 204 max_wrap_width, 205 ) 206 }); 207 208 let desc_font_max_size = max_desc_galley.rect.height(); 209 let title_font_max_size = max_title_galley.rect.height(); 210 title_font_max_size + desc_font_max_size + (2.0 * height_padding) 211 }; 212 213 let helper = AnimationHelper::new(ui, data.title, vec2(max_width, max_height)); 214 let animation_rect = helper.get_animation_rect(); 215 216 let cur_icon_width = helper.scale_1d_pos(min_icon_width); 217 let painter = ui.painter_at(animation_rect); 218 219 let cur_icon_size = vec2(cur_icon_width, cur_icon_width); 220 let cur_icon_x_pos = animation_rect.left() + (icon_padding) + (cur_icon_width / 2.0); 221 222 let title_cur_font = FontId::new( 223 helper.scale_1d_pos(title_min_font_size), 224 title_style.font_family(), 225 ); 226 227 let desc_cur_font = FontId::new( 228 helper.scale_1d_pos(desc_min_font_size), 229 desc_style.font_family(), 230 ); 231 232 let wrap_width = max_width - (cur_icon_width + (icon_padding * 2.0)); 233 let text_color = ui.ctx().style().visuals.text_color(); 234 let fallback_color = ui.ctx().style().visuals.weak_text_color(); 235 236 let title_galley = painter.layout( 237 data.title.to_string(), 238 title_cur_font, 239 text_color, 240 wrap_width, 241 ); 242 let desc_galley = painter.layout( 243 data.description.to_string(), 244 desc_cur_font, 245 text_color, 246 wrap_width, 247 ); 248 249 let galley_heights = title_galley.rect.height() + desc_galley.rect.height(); 250 251 let cur_height_padding = (animation_rect.height() - galley_heights) / 2.0; 252 let corner_x_pos = cur_icon_x_pos + (cur_icon_width / 2.0) + icon_padding; 253 let title_corner_pos = Pos2::new(corner_x_pos, animation_rect.top() + cur_height_padding); 254 let desc_corner_pos = Pos2::new( 255 corner_x_pos, 256 title_corner_pos.y + title_galley.rect.height(), 257 ); 258 259 let icon_cur_y = animation_rect.top() + cur_height_padding + (galley_heights / 2.0); 260 let icon_img = egui::Image::new(data.icon).fit_to_exact_size(cur_icon_size); 261 let icon_rect = Rect::from_center_size(pos2(cur_icon_x_pos, icon_cur_y), cur_icon_size); 262 263 icon_img.paint_at(ui, icon_rect); 264 painter.galley(title_corner_pos, title_galley, fallback_color); 265 painter.galley(desc_corner_pos, desc_galley, fallback_color); 266 267 helper.take_animation_response() 268 } 269 270 fn get_base_options(&self) -> Vec<ColumnOptionData> { 271 let mut vec = Vec::new(); 272 vec.push(ColumnOptionData { 273 title: "Universe", 274 description: "See the whole nostr universe", 275 icon: egui::include_image!("../../assets/icons/universe_icon_dark_4x.png"), 276 option: AddColumnOption::Universe, 277 }); 278 279 if let Some(acc) = self.cur_account { 280 let source = PubkeySource::Explicit(acc.pubkey); 281 282 vec.push(ColumnOptionData { 283 title: "Home timeline", 284 description: "See recommended notes first", 285 icon: egui::include_image!("../../assets/icons/home_icon_dark_4x.png"), 286 option: AddColumnOption::Home(source.clone()), 287 }); 288 } 289 vec.push(ColumnOptionData { 290 title: "Notifications", 291 description: "Stay up to date with notifications and mentions", 292 icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"), 293 option: AddColumnOption::UndecidedNotification, 294 }); 295 296 vec 297 } 298 299 fn get_notifications_options(&self) -> Vec<ColumnOptionData> { 300 let mut vec = Vec::new(); 301 302 if let Some(acc) = self.cur_account { 303 let source = if acc.secret_key.is_some() { 304 PubkeySource::DeckAuthor 305 } else { 306 PubkeySource::Explicit(acc.pubkey) 307 }; 308 309 vec.push(ColumnOptionData { 310 title: "Your Notifications", 311 description: "Stay up to date with your notifications and mentions", 312 icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"), 313 option: AddColumnOption::Notification(source), 314 }); 315 } 316 317 vec.push(ColumnOptionData { 318 title: "Someone else's Notifications", 319 description: "Stay up to date with someone else's notifications and mentions", 320 icon: egui::include_image!("../../assets/icons/notifications_icon_dark_4x.png"), 321 option: AddColumnOption::ExternalNotification, 322 }); 323 324 vec 325 } 326 } 327 328 struct ColumnOptionData { 329 title: &'static str, 330 description: &'static str, 331 icon: ImageSource<'static>, 332 option: AddColumnOption, 333 } 334 335 pub fn render_add_column_routes( 336 ui: &mut egui::Ui, 337 app: &mut Damus, 338 col: usize, 339 route: &AddColumnRoute, 340 ) { 341 let resp = match route { 342 AddColumnRoute::Base => AddColumnView::new( 343 &mut app.view_state.id_state_map, 344 &app.ndb, 345 app.accounts.get_selected_account(), 346 ) 347 .ui(ui), 348 AddColumnRoute::UndecidedNotification => AddColumnView::new( 349 &mut app.view_state.id_state_map, 350 &app.ndb, 351 app.accounts.get_selected_account(), 352 ) 353 .notifications_ui(ui), 354 AddColumnRoute::ExternalNotification => AddColumnView::new( 355 &mut app.view_state.id_state_map, 356 &app.ndb, 357 app.accounts.get_selected_account(), 358 ) 359 .external_notification_ui(ui), 360 }; 361 362 if let Some(resp) = resp { 363 match resp { 364 AddColumnResponse::Timeline(mut timeline) => { 365 crate::timeline::setup_new_timeline( 366 &mut timeline, 367 &app.ndb, 368 &mut app.subscriptions, 369 &mut app.pool, 370 &mut app.note_cache, 371 app.since_optimize, 372 ); 373 app.columns_mut().add_timeline_to_column(col, timeline); 374 } 375 AddColumnResponse::UndecidedNotification => { 376 app.columns_mut().column_mut(col).router_mut().route_to( 377 crate::route::Route::AddColumn(AddColumnRoute::UndecidedNotification), 378 ); 379 } 380 AddColumnResponse::ExternalNotification => { 381 app.columns_mut().column_mut(col).router_mut().route_to( 382 crate::route::Route::AddColumn(AddColumnRoute::ExternalNotification), 383 ); 384 } 385 }; 386 } 387 } 388 389 mod preview { 390 use crate::{ 391 test_data, 392 ui::{Preview, PreviewConfig, View}, 393 Damus, 394 }; 395 396 use super::AddColumnView; 397 398 pub struct AddColumnPreview { 399 app: Damus, 400 } 401 402 impl AddColumnPreview { 403 fn new() -> Self { 404 let app = test_data::test_app(); 405 406 AddColumnPreview { app } 407 } 408 } 409 410 impl View for AddColumnPreview { 411 fn ui(&mut self, ui: &mut egui::Ui) { 412 AddColumnView::new( 413 &mut self.app.view_state.id_state_map, 414 &self.app.ndb, 415 self.app.accounts.get_selected_account(), 416 ) 417 .ui(ui); 418 } 419 } 420 421 impl<'a> Preview for AddColumnView<'a> { 422 type Prev = AddColumnPreview; 423 424 fn preview(_cfg: PreviewConfig) -> Self::Prev { 425 AddColumnPreview::new() 426 } 427 } 428 }