mod.rs (15518B)
1 pub mod contents; 2 pub mod options; 3 pub mod post; 4 pub mod reply; 5 6 pub use contents::NoteContents; 7 pub use options::NoteOptions; 8 pub use post::{PostAction, PostResponse, PostView}; 9 pub use reply::PostReplyView; 10 11 use crate::{actionbar::BarAction, colors, notecache::CachedNote, ui, ui::View, Damus}; 12 use egui::{Label, RichText, Sense}; 13 use nostrdb::{Note, NoteKey, NoteReply, Transaction}; 14 15 pub struct NoteView<'a> { 16 app: &'a mut Damus, 17 note: &'a nostrdb::Note<'a>, 18 flags: NoteOptions, 19 } 20 21 pub struct NoteResponse { 22 pub response: egui::Response, 23 pub action: Option<BarAction>, 24 } 25 26 impl<'a> View for NoteView<'a> { 27 fn ui(&mut self, ui: &mut egui::Ui) { 28 self.show(ui); 29 } 30 } 31 32 fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app: &mut Damus) { 33 #[cfg(feature = "profiling")] 34 puffin::profile_function!(); 35 36 let size = 10.0; 37 let selectable = false; 38 39 ui.add( 40 Label::new( 41 RichText::new("replying to") 42 .size(size) 43 .color(colors::GRAY_SECONDARY), 44 ) 45 .selectable(selectable), 46 ); 47 48 let reply = if let Some(reply) = note_reply.reply() { 49 reply 50 } else { 51 return; 52 }; 53 54 let reply_note = if let Ok(reply_note) = app.ndb.get_note_by_id(txn, reply.id) { 55 reply_note 56 } else { 57 ui.add( 58 Label::new( 59 RichText::new("a note") 60 .size(size) 61 .color(colors::GRAY_SECONDARY), 62 ) 63 .selectable(selectable), 64 ); 65 return; 66 }; 67 68 if note_reply.is_reply_to_root() { 69 // We're replying to the root, let's show this 70 ui.add( 71 ui::Mention::new(app, txn, reply_note.pubkey()) 72 .size(size) 73 .selectable(selectable), 74 ); 75 ui.add( 76 Label::new( 77 RichText::new("'s note") 78 .size(size) 79 .color(colors::GRAY_SECONDARY), 80 ) 81 .selectable(selectable), 82 ); 83 } else if let Some(root) = note_reply.root() { 84 // replying to another post in a thread, not the root 85 86 if let Ok(root_note) = app.ndb.get_note_by_id(txn, root.id) { 87 if root_note.pubkey() == reply_note.pubkey() { 88 // simply "replying to bob's note" when replying to bob in his thread 89 ui.add( 90 ui::Mention::new(app, txn, reply_note.pubkey()) 91 .size(size) 92 .selectable(selectable), 93 ); 94 ui.add( 95 Label::new( 96 RichText::new("'s note") 97 .size(size) 98 .color(colors::GRAY_SECONDARY), 99 ) 100 .selectable(selectable), 101 ); 102 } else { 103 // replying to bob in alice's thread 104 105 ui.add( 106 ui::Mention::new(app, txn, reply_note.pubkey()) 107 .size(size) 108 .selectable(selectable), 109 ); 110 ui.add( 111 Label::new(RichText::new("in").size(size).color(colors::GRAY_SECONDARY)) 112 .selectable(selectable), 113 ); 114 ui.add( 115 ui::Mention::new(app, txn, root_note.pubkey()) 116 .size(size) 117 .selectable(selectable), 118 ); 119 ui.add( 120 Label::new( 121 RichText::new("'s thread") 122 .size(size) 123 .color(colors::GRAY_SECONDARY), 124 ) 125 .selectable(selectable), 126 ); 127 } 128 } else { 129 ui.add( 130 ui::Mention::new(app, txn, reply_note.pubkey()) 131 .size(size) 132 .selectable(selectable), 133 ); 134 ui.add( 135 Label::new( 136 RichText::new("in someone's thread") 137 .size(size) 138 .color(colors::GRAY_SECONDARY), 139 ) 140 .selectable(selectable), 141 ); 142 } 143 } 144 } 145 146 impl<'a> NoteView<'a> { 147 pub fn new(app: &'a mut Damus, note: &'a nostrdb::Note<'a>) -> Self { 148 let flags = NoteOptions::actionbar | NoteOptions::note_previews; 149 Self { app, note, flags } 150 } 151 152 pub fn actionbar(mut self, enable: bool) -> Self { 153 self.options_mut().set_actionbar(enable); 154 self 155 } 156 157 pub fn small_pfp(mut self, enable: bool) -> Self { 158 self.options_mut().set_small_pfp(enable); 159 self 160 } 161 162 pub fn medium_pfp(mut self, enable: bool) -> Self { 163 self.options_mut().set_medium_pfp(enable); 164 self 165 } 166 167 pub fn note_previews(mut self, enable: bool) -> Self { 168 self.options_mut().set_note_previews(enable); 169 self 170 } 171 172 pub fn selectable_text(mut self, enable: bool) -> Self { 173 self.options_mut().set_selectable_text(enable); 174 self 175 } 176 177 pub fn wide(mut self, enable: bool) -> Self { 178 self.options_mut().set_wide(enable); 179 self 180 } 181 182 pub fn options(&self) -> NoteOptions { 183 self.flags 184 } 185 186 pub fn options_mut(&mut self) -> &mut NoteOptions { 187 &mut self.flags 188 } 189 190 fn textmode_ui(&mut self, ui: &mut egui::Ui) -> egui::Response { 191 let note_key = self.note.key().expect("todo: implement non-db notes"); 192 let txn = self.note.txn().expect("todo: implement non-db notes"); 193 194 ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { 195 let profile = self.app.ndb.get_profile_by_pubkey(txn, self.note.pubkey()); 196 197 //ui.horizontal(|ui| { 198 ui.spacing_mut().item_spacing.x = 2.0; 199 200 let cached_note = self 201 .app 202 .note_cache_mut() 203 .cached_note_or_insert_mut(note_key, self.note); 204 205 let (_id, rect) = ui.allocate_space(egui::vec2(50.0, 20.0)); 206 ui.allocate_rect(rect, Sense::hover()); 207 ui.put(rect, |ui: &mut egui::Ui| { 208 render_reltime(ui, cached_note, false).response 209 }); 210 let (_id, rect) = ui.allocate_space(egui::vec2(150.0, 20.0)); 211 ui.allocate_rect(rect, Sense::hover()); 212 ui.put(rect, |ui: &mut egui::Ui| { 213 ui.add( 214 ui::Username::new(profile.as_ref().ok(), self.note.pubkey()) 215 .abbreviated(6) 216 .pk_colored(true), 217 ) 218 }); 219 220 ui.add(NoteContents::new( 221 self.app, txn, self.note, note_key, self.flags, 222 )); 223 //}); 224 }) 225 .response 226 } 227 228 pub fn expand_size() -> f32 { 229 5.0 230 } 231 232 fn pfp( 233 &mut self, 234 note_key: NoteKey, 235 profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>, 236 ui: &mut egui::Ui, 237 ) { 238 if !self.options().has_wide() { 239 ui.spacing_mut().item_spacing.x = 16.0; 240 } else { 241 ui.spacing_mut().item_spacing.x = 4.0; 242 } 243 244 let pfp_size = self.options().pfp_size(); 245 246 match profile 247 .as_ref() 248 .ok() 249 .and_then(|p| p.record().profile()?.picture()) 250 { 251 // these have different lifetimes and types, 252 // so the calls must be separate 253 Some(pic) => { 254 let anim_speed = 0.05; 255 let profile_key = profile.as_ref().unwrap().record().note_key(); 256 let note_key = note_key.as_u64(); 257 258 if self.app.is_mobile() { 259 ui.add(ui::ProfilePic::new(&mut self.app.img_cache, pic)); 260 } else { 261 let (rect, size, _resp) = ui::anim::hover_expand( 262 ui, 263 egui::Id::new((profile_key, note_key)), 264 pfp_size, 265 ui::NoteView::expand_size(), 266 anim_speed, 267 ); 268 269 ui.put( 270 rect, 271 ui::ProfilePic::new(&mut self.app.img_cache, pic).size(size), 272 ) 273 .on_hover_ui_at_pointer(|ui| { 274 ui.set_max_width(300.0); 275 ui.add(ui::ProfilePreview::new( 276 profile.as_ref().unwrap(), 277 &mut self.app.img_cache, 278 )); 279 }); 280 } 281 } 282 None => { 283 ui.add( 284 ui::ProfilePic::new(&mut self.app.img_cache, ui::ProfilePic::no_pfp_url()) 285 .size(pfp_size), 286 ); 287 } 288 } 289 } 290 291 pub fn show(&mut self, ui: &mut egui::Ui) -> NoteResponse { 292 if self.app.textmode { 293 NoteResponse { 294 response: self.textmode_ui(ui), 295 action: None, 296 } 297 } else { 298 self.show_standard(ui) 299 } 300 } 301 302 fn note_header( 303 ui: &mut egui::Ui, 304 app: &mut Damus, 305 note: &Note, 306 profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>, 307 ) -> egui::Response { 308 let note_key = note.key().unwrap(); 309 310 ui.horizontal(|ui| { 311 ui.spacing_mut().item_spacing.x = 2.0; 312 ui.add(ui::Username::new(profile.as_ref().ok(), note.pubkey()).abbreviated(20)); 313 314 let cached_note = app 315 .note_cache_mut() 316 .cached_note_or_insert_mut(note_key, note); 317 render_reltime(ui, cached_note, true); 318 }) 319 .response 320 } 321 322 fn show_standard(&mut self, ui: &mut egui::Ui) -> NoteResponse { 323 #[cfg(feature = "profiling")] 324 puffin::profile_function!(); 325 let note_key = self.note.key().expect("todo: support non-db notes"); 326 let txn = self.note.txn().expect("todo: support non-db notes"); 327 let mut note_action: Option<BarAction> = None; 328 let profile = self.app.ndb.get_profile_by_pubkey(txn, self.note.pubkey()); 329 330 // wide design 331 let response = if self.options().has_wide() { 332 ui.horizontal(|ui| { 333 self.pfp(note_key, &profile, ui); 334 335 let size = ui.available_size(); 336 ui.vertical(|ui| { 337 ui.add_sized([size.x, self.options().pfp_size()], |ui: &mut egui::Ui| { 338 ui.horizontal_centered(|ui| { 339 NoteView::note_header(ui, self.app, self.note, &profile); 340 }) 341 .response 342 }); 343 344 let note_reply = self 345 .app 346 .note_cache_mut() 347 .cached_note_or_insert_mut(note_key, self.note) 348 .reply 349 .borrow(self.note.tags()); 350 351 if note_reply.reply().is_some() { 352 ui.horizontal(|ui| { 353 reply_desc(ui, txn, ¬e_reply, self.app); 354 }); 355 } 356 }); 357 }); 358 359 let resp = ui.add(NoteContents::new( 360 self.app, 361 txn, 362 self.note, 363 note_key, 364 self.options(), 365 )); 366 367 if self.options().has_actionbar() { 368 note_action = render_note_actionbar(ui, note_key).inner; 369 } 370 371 resp 372 } else { 373 // main design 374 ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { 375 self.pfp(note_key, &profile, ui); 376 377 ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { 378 NoteView::note_header(ui, self.app, self.note, &profile); 379 380 ui.horizontal(|ui| { 381 ui.spacing_mut().item_spacing.x = 2.0; 382 383 let note_reply = self 384 .app 385 .note_cache_mut() 386 .cached_note_or_insert_mut(note_key, self.note) 387 .reply 388 .borrow(self.note.tags()); 389 390 if note_reply.reply().is_some() { 391 reply_desc(ui, txn, ¬e_reply, self.app); 392 } 393 }); 394 395 ui.add(NoteContents::new( 396 self.app, 397 txn, 398 self.note, 399 note_key, 400 self.options(), 401 )); 402 403 if self.options().has_actionbar() { 404 note_action = render_note_actionbar(ui, note_key).inner; 405 } 406 }); 407 }) 408 .response 409 }; 410 411 NoteResponse { 412 response, 413 action: note_action, 414 } 415 } 416 } 417 418 fn render_note_actionbar( 419 ui: &mut egui::Ui, 420 note_key: NoteKey, 421 ) -> egui::InnerResponse<Option<BarAction>> { 422 ui.horizontal(|ui| { 423 let reply_resp = reply_button(ui, note_key); 424 let thread_resp = thread_button(ui, note_key); 425 426 if reply_resp.clicked() { 427 Some(BarAction::Reply) 428 } else if thread_resp.clicked() { 429 Some(BarAction::OpenThread) 430 } else { 431 None 432 } 433 }) 434 } 435 436 fn secondary_label(ui: &mut egui::Ui, s: impl Into<String>) { 437 ui.add(Label::new( 438 RichText::new(s).size(10.0).color(colors::GRAY_SECONDARY), 439 )); 440 } 441 442 fn render_reltime( 443 ui: &mut egui::Ui, 444 note_cache: &mut CachedNote, 445 before: bool, 446 ) -> egui::InnerResponse<()> { 447 #[cfg(feature = "profiling")] 448 puffin::profile_function!(); 449 450 ui.horizontal(|ui| { 451 if before { 452 secondary_label(ui, "⋅"); 453 } 454 455 secondary_label(ui, note_cache.reltime_str_mut()); 456 457 if !before { 458 secondary_label(ui, "⋅"); 459 } 460 }) 461 } 462 463 fn reply_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { 464 let img_data = if ui.style().visuals.dark_mode { 465 egui::include_image!("../../../assets/icons/reply.png") 466 } else { 467 egui::include_image!("../../../assets/icons/reply-dark.png") 468 }; 469 470 let (rect, size, resp) = 471 ui::anim::hover_expand_small(ui, ui.id().with(("reply_anim", note_key))); 472 473 // align rect to note contents 474 let expand_size = 5.0; // from hover_expand_small 475 let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0)); 476 477 let put_resp = ui.put(rect, egui::Image::new(img_data).max_width(size)); 478 479 resp.union(put_resp) 480 } 481 482 fn thread_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response { 483 let id = ui.id().with(("thread_anim", note_key)); 484 let size = 8.0; 485 let expand_size = 5.0; 486 let anim_speed = 0.05; 487 488 let (rect, size, resp) = ui::anim::hover_expand(ui, id, size, expand_size, anim_speed); 489 490 let color = if ui.style().visuals.dark_mode { 491 egui::Color32::WHITE 492 } else { 493 egui::Color32::BLACK 494 }; 495 496 ui.painter_at(rect).circle_stroke( 497 rect.center(), 498 (size - 1.0) / 2.0, 499 egui::Stroke::new(1.0, color), 500 ); 501 502 resp 503 }