post.rs (26550B)
1 use crate::draft::{Draft, Drafts, MentionHint}; 2 #[cfg(not(target_os = "android"))] 3 use crate::media_upload::{nostrbuild_nip96_upload, MediaPath}; 4 use crate::post::{downcast_post_buffer, MentionType, NewPost}; 5 use crate::ui::search_results::SearchResultsView; 6 use crate::ui::{self, Preview, PreviewConfig}; 7 use crate::Result; 8 9 use egui::{ 10 text::{CCursorRange, LayoutJob}, 11 text_edit::TextEditOutput, 12 widgets::text_edit::TextEdit, 13 Frame, Layout, Margin, Pos2, ScrollArea, Sense, TextBuffer, 14 }; 15 use enostr::{FilledKeypair, FullKeypair, NoteId, Pubkey, RelayPool}; 16 use nostrdb::{Ndb, Transaction}; 17 use notedeck_ui::{ 18 app_images, 19 blur::PixelDimensions, 20 context_menu::{input_context, PasteBehavior}, 21 gif::{handle_repaint, retrieve_latest_texture}, 22 images::{get_render_state, RenderState}, 23 jobs::JobsCache, 24 note::render_note_preview, 25 NoteOptions, ProfilePic, 26 }; 27 28 use notedeck::{ 29 name::get_display_name, supported_mime_hosted_at_url, tr, Localization, NoteAction, NoteContext, 30 }; 31 use tracing::error; 32 33 pub struct PostView<'a, 'd> { 34 note_context: &'a mut NoteContext<'d>, 35 draft: &'a mut Draft, 36 post_type: PostType, 37 poster: FilledKeypair<'a>, 38 id_source: Option<egui::Id>, 39 inner_rect: egui::Rect, 40 note_options: NoteOptions, 41 jobs: &'a mut JobsCache, 42 } 43 44 #[derive(Clone)] 45 pub enum PostType { 46 New, 47 Quote(NoteId), 48 Reply(NoteId), 49 } 50 51 pub enum PostAction { 52 /// The NoteAction on a note you are replying to. 53 QuotedNoteAction(NoteAction), 54 55 /// The reply/new post action 56 NewPostAction(NewPostAction), 57 } 58 59 pub struct NewPostAction { 60 post_type: PostType, 61 post: NewPost, 62 } 63 64 impl NewPostAction { 65 pub fn new(post_type: PostType, post: NewPost) -> Self { 66 NewPostAction { post_type, post } 67 } 68 69 pub fn execute( 70 &self, 71 ndb: &Ndb, 72 txn: &Transaction, 73 pool: &mut RelayPool, 74 drafts: &mut Drafts, 75 ) -> Result<()> { 76 let seckey = self.post.account.secret_key.to_secret_bytes(); 77 78 let note = match self.post_type { 79 PostType::New => self.post.to_note(&seckey), 80 81 PostType::Reply(target) => { 82 let replying_to = ndb.get_note_by_id(txn, target.bytes())?; 83 self.post.to_reply(&seckey, &replying_to) 84 } 85 86 PostType::Quote(target) => { 87 let quoting = ndb.get_note_by_id(txn, target.bytes())?; 88 self.post.to_quote(&seckey, "ing) 89 } 90 }; 91 92 pool.send(&enostr::ClientMessage::event(¬e)?); 93 drafts.get_from_post_type(&self.post_type).clear(); 94 95 Ok(()) 96 } 97 } 98 99 pub struct PostResponse { 100 pub action: Option<PostAction>, 101 pub edit_response: egui::Response, 102 } 103 104 impl<'a, 'd> PostView<'a, 'd> { 105 #[allow(clippy::too_many_arguments)] 106 pub fn new( 107 note_context: &'a mut NoteContext<'d>, 108 draft: &'a mut Draft, 109 post_type: PostType, 110 poster: FilledKeypair<'a>, 111 inner_rect: egui::Rect, 112 note_options: NoteOptions, 113 jobs: &'a mut JobsCache, 114 ) -> Self { 115 let id_source: Option<egui::Id> = None; 116 PostView { 117 note_context, 118 draft, 119 poster, 120 id_source, 121 post_type, 122 inner_rect, 123 note_options, 124 jobs, 125 } 126 } 127 128 pub fn id_source(mut self, id_source: impl std::hash::Hash) -> Self { 129 self.id_source = Some(egui::Id::new(id_source)); 130 self 131 } 132 133 fn editbox(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> egui::Response { 134 ui.spacing_mut().item_spacing.x = 12.0; 135 136 let pfp_size = 24.0; 137 138 // TODO: refactor pfp control to do all of this for us 139 let poster_pfp = self 140 .note_context 141 .ndb 142 .get_profile_by_pubkey(txn, self.poster.pubkey.bytes()) 143 .as_ref() 144 .ok() 145 .and_then(|p| { 146 Some(ProfilePic::from_profile(self.note_context.img_cache, p)?.size(pfp_size)) 147 }); 148 149 if let Some(mut pfp) = poster_pfp { 150 ui.add(&mut pfp); 151 } else { 152 ui.add( 153 &mut ProfilePic::new(self.note_context.img_cache, notedeck::profile::no_pfp_url()) 154 .size(pfp_size), 155 ); 156 } 157 158 let mut updated_layout = false; 159 let mut layouter = |ui: &egui::Ui, buf: &dyn TextBuffer, wrap_width: f32| { 160 if let Some(post_buffer) = downcast_post_buffer(buf) { 161 let maybe_job = if post_buffer.need_new_layout(self.draft.cur_layout.as_ref()) { 162 Some(post_buffer.to_layout_job(ui)) 163 } else { 164 None 165 }; 166 167 if let Some(job) = maybe_job { 168 self.draft.cur_layout = Some((post_buffer.text_buffer.clone(), job)); 169 updated_layout = true; 170 } 171 }; 172 173 let mut layout_job = if let Some((_, job)) = &self.draft.cur_layout { 174 job.clone() 175 } else { 176 error!("Failed to get custom mentions layouter"); 177 text_edit_default_layout(ui, buf.as_str().to_owned(), wrap_width) 178 }; 179 180 layout_job.wrap.max_width = wrap_width; 181 ui.fonts(|f| f.layout_job(layout_job)) 182 }; 183 184 let textedit = TextEdit::multiline(&mut self.draft.buffer) 185 .hint_text( 186 egui::RichText::new(tr!( 187 self.note_context.i18n, 188 "Write a banger note here...", 189 "Placeholder for note input field" 190 )) 191 .weak(), 192 ) 193 .frame(false) 194 .desired_width(ui.available_width()) 195 .layouter(&mut layouter); 196 197 let out = textedit.show(ui); 198 199 input_context( 200 &out.response, 201 self.note_context.clipboard, 202 &mut self.draft.buffer.text_buffer, 203 PasteBehavior::Append, 204 ); 205 206 if updated_layout { 207 self.draft.buffer.selected_mention = false; 208 } 209 210 if let Some(cursor_index) = get_cursor_index(&out.state.cursor.char_range()) { 211 self.show_mention_hints(txn, ui, cursor_index, &out); 212 } 213 214 let focused = out.response.has_focus(); 215 216 ui.ctx().data_mut(|d| d.insert_temp(self.id(), focused)); 217 218 out.response 219 } 220 221 fn show_mention_hints( 222 &mut self, 223 txn: &nostrdb::Transaction, 224 ui: &mut egui::Ui, 225 cursor_index: usize, 226 textedit_output: &TextEditOutput, 227 ) { 228 let Some(mention) = self.draft.buffer.get_mention(cursor_index) else { 229 return; 230 }; 231 232 if mention.info.mention_type != MentionType::Pending { 233 return; 234 } 235 236 if ui.ctx().input(|r| r.key_pressed(egui::Key::Escape)) { 237 self.draft.buffer.delete_mention(mention.index); 238 return; 239 } 240 241 let mention_str = self.draft.buffer.get_mention_string(&mention); 242 243 if !mention_str.is_empty() { 244 if let Some(mention_hint) = &mut self.draft.cur_mention_hint { 245 if mention_hint.index != mention.index { 246 mention_hint.index = mention.index; 247 mention_hint.pos = 248 calculate_mention_hints_pos(textedit_output, mention.info.start_index); 249 } 250 mention_hint.text = mention_str.to_owned(); 251 } else { 252 self.draft.cur_mention_hint = Some(MentionHint { 253 index: mention.index, 254 text: mention_str.to_owned(), 255 pos: calculate_mention_hints_pos(textedit_output, mention.info.start_index), 256 }); 257 } 258 } 259 260 let hint_rect = { 261 let hint = if let Some(hint) = &self.draft.cur_mention_hint { 262 hint 263 } else { 264 return; 265 }; 266 267 let mut hint_rect = self.inner_rect; 268 hint_rect.set_top(hint.pos.y); 269 hint_rect 270 }; 271 272 let Ok(res) = self.note_context.ndb.search_profile(txn, mention_str, 10) else { 273 return; 274 }; 275 276 let resp = SearchResultsView::new( 277 self.note_context.img_cache, 278 self.note_context.ndb, 279 txn, 280 &res, 281 ) 282 .show_in_rect(hint_rect, ui); 283 284 match resp { 285 ui::search_results::SearchResultsResponse::SelectResult(selection) => { 286 if let Some(hint_index) = selection { 287 if let Some(pk) = res.get(hint_index) { 288 let record = self.note_context.ndb.get_profile_by_pubkey(txn, pk); 289 290 self.draft.buffer.select_mention_and_replace_name( 291 mention.index, 292 get_display_name(record.ok().as_ref()).name(), 293 Pubkey::new(**pk), 294 ); 295 self.draft.cur_mention_hint = None; 296 } 297 } 298 } 299 300 ui::search_results::SearchResultsResponse::DeleteMention => { 301 self.draft.buffer.delete_mention(mention.index) 302 } 303 } 304 } 305 306 fn focused(&self, ui: &egui::Ui) -> bool { 307 ui.ctx() 308 .data(|d| d.get_temp::<bool>(self.id()).unwrap_or(false)) 309 } 310 311 fn id(&self) -> egui::Id { 312 self.id_source.unwrap_or_else(|| egui::Id::new("post")) 313 } 314 315 pub fn outer_margin() -> i8 { 316 16 317 } 318 319 pub fn inner_margin() -> i8 { 320 12 321 } 322 323 pub fn ui(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse { 324 let focused = self.focused(ui); 325 let stroke = if focused { 326 ui.visuals().selection.stroke 327 } else { 328 ui.visuals().noninteractive().bg_stroke 329 }; 330 331 let mut frame = egui::Frame::default() 332 .inner_margin(egui::Margin::same(PostView::inner_margin())) 333 .outer_margin(egui::Margin::same(PostView::outer_margin())) 334 .fill(ui.visuals().extreme_bg_color) 335 .stroke(stroke) 336 .corner_radius(12.0); 337 338 if focused { 339 frame = frame.shadow(egui::epaint::Shadow { 340 offset: [0, 0], 341 blur: 8, 342 spread: 0, 343 color: stroke.color, 344 }); 345 } 346 347 frame 348 .show(ui, |ui| ui.vertical(|ui| self.input_ui(txn, ui)).inner) 349 .inner 350 } 351 352 fn input_ui(&mut self, txn: &Transaction, ui: &mut egui::Ui) -> PostResponse { 353 let edit_response = ui.horizontal(|ui| self.editbox(txn, ui)).inner; 354 355 let note_response = if let PostType::Quote(id) = self.post_type { 356 let avail_size = ui.available_size_before_wrap(); 357 Some( 358 ui.with_layout(Layout::left_to_right(egui::Align::TOP), |ui| { 359 Frame::NONE 360 .show(ui, |ui| { 361 ui.vertical(|ui| { 362 ui.set_max_width(avail_size.x * 0.8); 363 364 render_note_preview( 365 ui, 366 self.note_context, 367 txn, 368 id.bytes(), 369 nostrdb::NoteKey::new(0), 370 self.note_options, 371 self.jobs, 372 ) 373 }) 374 .inner 375 }) 376 .inner 377 }) 378 .inner, 379 ) 380 } else { 381 None 382 }; 383 384 Frame::new() 385 .inner_margin(Margin::symmetric(0, 8)) 386 .show(ui, |ui| { 387 ScrollArea::horizontal().show(ui, |ui| { 388 ui.with_layout(Layout::left_to_right(egui::Align::Min), |ui| { 389 ui.add_space(4.0); 390 self.show_media(ui); 391 }); 392 }); 393 }); 394 395 self.transfer_uploads(ui); 396 self.show_upload_errors(ui); 397 398 let post_action = ui.horizontal(|ui| self.input_buttons(ui)).inner; 399 400 let action = note_response 401 .and_then(|nr| nr.action.map(PostAction::QuotedNoteAction)) 402 .or(post_action.map(PostAction::NewPostAction)); 403 404 PostResponse { 405 action, 406 edit_response, 407 } 408 } 409 410 fn input_buttons(&mut self, ui: &mut egui::Ui) -> Option<NewPostAction> { 411 ui.with_layout(egui::Layout::left_to_right(egui::Align::BOTTOM), |ui| { 412 self.show_upload_media_button(ui); 413 }); 414 415 ui.with_layout(egui::Layout::right_to_left(egui::Align::BOTTOM), |ui| { 416 let post_button_clicked = ui 417 .add_sized( 418 [91.0, 32.0], 419 post_button(self.note_context.i18n, !self.draft.buffer.is_empty()), 420 ) 421 .clicked(); 422 423 let shortcut_pressed = ui.input(|i| { 424 (i.modifiers.ctrl || i.modifiers.command) && i.key_pressed(egui::Key::Enter) 425 }); 426 427 if post_button_clicked 428 || (!self.draft.buffer.is_empty() && shortcut_pressed && self.focused(ui)) 429 { 430 let output = self.draft.buffer.output(); 431 let new_post = NewPost::new( 432 output.text, 433 self.poster.to_full(), 434 self.draft.uploaded_media.clone(), 435 output.mentions, 436 ); 437 Some(NewPostAction::new(self.post_type.clone(), new_post)) 438 } else { 439 None 440 } 441 }) 442 .inner 443 } 444 445 fn show_media(&mut self, ui: &mut egui::Ui) { 446 let mut to_remove = Vec::new(); 447 for (i, media) in self.draft.uploaded_media.iter().enumerate() { 448 let (width, height) = if let Some(dims) = media.dimensions { 449 (dims.0, dims.1) 450 } else { 451 (300, 300) 452 }; 453 454 let Some(cache_type) = 455 supported_mime_hosted_at_url(&mut self.note_context.img_cache.urls, &media.url) 456 else { 457 self.draft 458 .upload_errors 459 .push("Uploaded media is not supported.".to_owned()); 460 error!("Unsupported mime type at url: {}", &media.url); 461 continue; 462 }; 463 464 let url = &media.url; 465 let cur_state = get_render_state( 466 ui.ctx(), 467 self.note_context.img_cache, 468 cache_type, 469 url, 470 notedeck_ui::images::ImageType::Content, 471 ); 472 473 render_post_view_media( 474 ui, 475 &mut self.draft.upload_errors, 476 &mut to_remove, 477 i, 478 width, 479 height, 480 cur_state, 481 url, 482 ) 483 } 484 to_remove.reverse(); 485 for i in to_remove { 486 self.draft.uploaded_media.remove(i); 487 } 488 } 489 490 fn show_upload_media_button(&mut self, ui: &mut egui::Ui) { 491 if ui.add(media_upload_button()).clicked() { 492 #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] 493 { 494 if let Some(files) = rfd::FileDialog::new().pick_files() { 495 for file in files { 496 match MediaPath::new(file) { 497 Ok(media_path) => { 498 let promise = nostrbuild_nip96_upload( 499 self.poster.secret_key.secret_bytes(), 500 media_path, 501 ); 502 self.draft.uploading_media.push(promise); 503 } 504 Err(e) => { 505 error!("{e}"); 506 self.draft.upload_errors.push(e.to_string()); 507 } 508 } 509 } 510 } 511 } 512 } 513 } 514 515 fn transfer_uploads(&mut self, ui: &mut egui::Ui) { 516 let mut indexes_to_remove = Vec::new(); 517 for (i, promise) in self.draft.uploading_media.iter().enumerate() { 518 match promise.ready() { 519 Some(Ok(media)) => { 520 self.draft.uploaded_media.push(media.clone()); 521 indexes_to_remove.push(i); 522 } 523 Some(Err(e)) => { 524 self.draft.upload_errors.push(e.to_string()); 525 error!("{e}"); 526 } 527 None => { 528 ui.spinner(); 529 } 530 } 531 } 532 533 indexes_to_remove.reverse(); 534 for i in indexes_to_remove { 535 let _ = self.draft.uploading_media.remove(i); 536 } 537 } 538 539 fn show_upload_errors(&mut self, ui: &mut egui::Ui) { 540 let mut to_remove = Vec::new(); 541 for (i, error) in self.draft.upload_errors.iter().enumerate() { 542 if ui 543 .add( 544 egui::Label::new(egui::RichText::new(error).color(ui.visuals().warn_fg_color)) 545 .sense(Sense::click()) 546 .selectable(false), 547 ) 548 .on_hover_text_at_pointer("Dismiss") 549 .clicked() 550 { 551 to_remove.push(i); 552 } 553 } 554 to_remove.reverse(); 555 556 for i in to_remove { 557 self.draft.upload_errors.remove(i); 558 } 559 } 560 } 561 562 #[allow(clippy::too_many_arguments)] 563 fn render_post_view_media( 564 ui: &mut egui::Ui, 565 upload_errors: &mut Vec<String>, 566 to_remove: &mut Vec<usize>, 567 cur_index: usize, 568 width: u32, 569 height: u32, 570 render_state: RenderState, 571 url: &str, 572 ) { 573 match render_state.texture_state { 574 notedeck::TextureState::Pending => { 575 ui.spinner(); 576 } 577 notedeck::TextureState::Error(e) => { 578 upload_errors.push(e.to_string()); 579 error!("{e}"); 580 } 581 notedeck::TextureState::Loaded(renderable_media) => { 582 let max_size = 300; 583 let size = if width > max_size || height > max_size { 584 PixelDimensions { x: 300, y: 300 } 585 } else { 586 PixelDimensions { 587 x: width, 588 y: height, 589 } 590 } 591 .to_points(ui.pixels_per_point()) 592 .to_vec(); 593 594 let texture_handle = handle_repaint( 595 ui, 596 retrieve_latest_texture(url, render_state.gifs, renderable_media), 597 ); 598 let img_resp = ui.add( 599 egui::Image::new(texture_handle) 600 .max_size(size) 601 .corner_radius(12.0), 602 ); 603 604 let remove_button_rect = { 605 let top_left = img_resp.rect.left_top(); 606 let spacing = 13.0; 607 let center = Pos2::new(top_left.x + spacing, top_left.y + spacing); 608 egui::Rect::from_center_size(center, egui::vec2(26.0, 26.0)) 609 }; 610 if show_remove_upload_button(ui, remove_button_rect).clicked() { 611 to_remove.push(cur_index); 612 } 613 ui.advance_cursor_after_rect(img_resp.rect); 614 } 615 } 616 } 617 618 fn post_button<'a>(i18n: &'a mut Localization, interactive: bool) -> impl egui::Widget + 'a { 619 move |ui: &mut egui::Ui| { 620 let button = egui::Button::new(tr!(i18n, "Post now", "Button label to post a note")); 621 if interactive { 622 ui.add(button) 623 } else { 624 ui.add( 625 button 626 .sense(egui::Sense::hover()) 627 .fill(ui.visuals().widgets.noninteractive.bg_fill) 628 .stroke(ui.visuals().widgets.noninteractive.bg_stroke), 629 ) 630 .on_hover_cursor(egui::CursorIcon::NotAllowed) 631 } 632 } 633 } 634 635 fn media_upload_button() -> impl egui::Widget { 636 |ui: &mut egui::Ui| -> egui::Response { 637 let resp = ui.allocate_response(egui::vec2(32.0, 32.0), egui::Sense::click()); 638 let painter = ui.painter(); 639 let (fill_color, stroke) = if resp.hovered() { 640 ( 641 ui.visuals().widgets.hovered.bg_fill, 642 ui.visuals().widgets.hovered.bg_stroke, 643 ) 644 } else if resp.clicked() { 645 ( 646 ui.visuals().widgets.active.bg_fill, 647 ui.visuals().widgets.active.bg_stroke, 648 ) 649 } else { 650 ( 651 ui.visuals().widgets.inactive.bg_fill, 652 ui.visuals().widgets.inactive.bg_stroke, 653 ) 654 }; 655 656 painter.rect_filled(resp.rect, 8.0, fill_color); 657 painter.rect_stroke(resp.rect, 8.0, stroke, egui::StrokeKind::Middle); 658 659 let upload_img = if ui.visuals().dark_mode { 660 app_images::media_upload_dark_image() 661 } else { 662 app_images::media_upload_light_image() 663 }; 664 665 upload_img 666 .max_size(egui::vec2(16.0, 16.0)) 667 .paint_at(ui, resp.rect.shrink(8.0)); 668 669 resp 670 } 671 } 672 673 fn show_remove_upload_button(ui: &mut egui::Ui, desired_rect: egui::Rect) -> egui::Response { 674 let resp = ui.allocate_rect(desired_rect, egui::Sense::click()); 675 let size = 24.0; 676 let (fill_color, stroke) = if resp.hovered() { 677 ( 678 ui.visuals().widgets.hovered.bg_fill, 679 ui.visuals().widgets.hovered.bg_stroke, 680 ) 681 } else if resp.clicked() { 682 ( 683 ui.visuals().widgets.active.bg_fill, 684 ui.visuals().widgets.active.bg_stroke, 685 ) 686 } else { 687 ( 688 ui.visuals().widgets.inactive.bg_fill, 689 ui.visuals().widgets.inactive.bg_stroke, 690 ) 691 }; 692 let center = desired_rect.center(); 693 let painter = ui.painter_at(desired_rect); 694 let radius = size / 2.0; 695 696 painter.circle_filled(center, radius, fill_color); 697 painter.circle_stroke(center, radius, stroke); 698 699 painter.line_segment( 700 [ 701 Pos2::new(center.x - 4.0, center.y - 4.0), 702 Pos2::new(center.x + 4.0, center.y + 4.0), 703 ], 704 egui::Stroke::new(1.33, ui.visuals().text_color()), 705 ); 706 707 painter.line_segment( 708 [ 709 Pos2::new(center.x + 4.0, center.y - 4.0), 710 Pos2::new(center.x - 4.0, center.y + 4.0), 711 ], 712 egui::Stroke::new(1.33, ui.visuals().text_color()), 713 ); 714 resp 715 } 716 717 fn get_cursor_index(cursor: &Option<CCursorRange>) -> Option<usize> { 718 let range = cursor.as_ref()?; 719 720 if range.primary.index == range.secondary.index { 721 Some(range.primary.index) 722 } else { 723 None 724 } 725 } 726 727 fn calculate_mention_hints_pos(out: &TextEditOutput, char_pos: usize) -> egui::Pos2 { 728 let mut cur_pos = 0; 729 730 for row in &out.galley.rows { 731 if cur_pos + row.glyphs.len() <= char_pos { 732 cur_pos += row.glyphs.len(); 733 } else if let Some(glyph) = row.glyphs.get(char_pos - cur_pos) { 734 let mut pos = glyph.pos + out.galley_pos.to_vec2(); 735 pos.y += row.rect.height(); 736 return pos; 737 } 738 } 739 740 out.text_clip_rect.left_bottom() 741 } 742 743 fn text_edit_default_layout(ui: &egui::Ui, text: String, wrap_width: f32) -> LayoutJob { 744 LayoutJob::simple( 745 text, 746 egui::FontSelection::default().resolve(ui.style()), 747 ui.visuals() 748 .override_text_color 749 .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color()), 750 wrap_width, 751 ) 752 } 753 754 mod preview { 755 756 use crate::media_upload::Nip94Event; 757 758 use super::*; 759 use notedeck::{App, AppAction, AppContext}; 760 761 pub struct PostPreview { 762 draft: Draft, 763 poster: FullKeypair, 764 jobs: JobsCache, 765 } 766 767 impl PostPreview { 768 fn new() -> Self { 769 let mut draft = Draft::new(); 770 // can use any url here 771 draft.uploaded_media.push(Nip94Event::new( 772 "https://image.nostr.build/41b40657dd6abf7c275dffc86b29bd863e9337a74870d4ee1c33a72a91c9d733.jpg".to_owned(), 773 612, 774 407, 775 )); 776 draft.uploaded_media.push(Nip94Event::new( 777 "https://image.nostr.build/thumb/fdb46182b039d29af0f5eac084d4d30cd4ad2580ea04fe6c7e79acfe095f9852.png".to_owned(), 778 80, 779 80, 780 )); 781 draft.uploaded_media.push(Nip94Event::new( 782 "https://i.nostr.build/7EznpHsnBZ36Akju.png".to_owned(), 783 2438, 784 1476, 785 )); 786 draft.uploaded_media.push(Nip94Event::new( 787 "https://i.nostr.build/qCCw8szrjTydTiMV.png".to_owned(), 788 2002, 789 2272, 790 )); 791 PostPreview { 792 draft, 793 poster: FullKeypair::generate(), 794 jobs: Default::default(), 795 } 796 } 797 } 798 799 impl App for PostPreview { 800 fn update(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> { 801 let txn = Transaction::new(app.ndb).expect("txn"); 802 let mut note_context = NoteContext { 803 ndb: app.ndb, 804 accounts: app.accounts, 805 global_wallet: app.global_wallet, 806 img_cache: app.img_cache, 807 note_cache: app.note_cache, 808 zaps: app.zaps, 809 pool: app.pool, 810 job_pool: app.job_pool, 811 unknown_ids: app.unknown_ids, 812 clipboard: app.clipboard, 813 i18n: app.i18n, 814 }; 815 816 PostView::new( 817 &mut note_context, 818 &mut self.draft, 819 PostType::New, 820 self.poster.to_filled(), 821 ui.available_rect_before_wrap(), 822 NoteOptions::default(), 823 &mut self.jobs, 824 ) 825 .ui(&txn, ui); 826 827 None 828 } 829 } 830 831 impl Preview for PostView<'_, '_> { 832 type Prev = PostPreview; 833 834 fn preview(_cfg: PreviewConfig) -> Self::Prev { 835 PostPreview::new() 836 } 837 } 838 }