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