notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

contents.rs (10481B)


      1 use crate::actionbar::NoteActionResponse;
      2 use crate::images::ImageType;
      3 use crate::imgcache::ImageCache;
      4 use crate::notecache::NoteCache;
      5 use crate::ui::note::{NoteOptions, NoteResponse};
      6 use crate::ui::ProfilePic;
      7 use crate::{colors, ui};
      8 use egui::{Color32, Hyperlink, Image, RichText};
      9 use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction};
     10 use tracing::warn;
     11 
     12 pub struct NoteContents<'a> {
     13     ndb: &'a Ndb,
     14     img_cache: &'a mut ImageCache,
     15     note_cache: &'a mut NoteCache,
     16     txn: &'a Transaction,
     17     note: &'a Note<'a>,
     18     note_key: NoteKey,
     19     options: NoteOptions,
     20     action: NoteActionResponse,
     21 }
     22 
     23 impl<'a> NoteContents<'a> {
     24     pub fn new(
     25         ndb: &'a Ndb,
     26         img_cache: &'a mut ImageCache,
     27         note_cache: &'a mut NoteCache,
     28         txn: &'a Transaction,
     29         note: &'a Note,
     30         note_key: NoteKey,
     31         options: ui::note::NoteOptions,
     32     ) -> Self {
     33         NoteContents {
     34             ndb,
     35             img_cache,
     36             note_cache,
     37             txn,
     38             note,
     39             note_key,
     40             options,
     41             action: NoteActionResponse::default(),
     42         }
     43     }
     44 
     45     pub fn action(&self) -> &NoteActionResponse {
     46         &self.action
     47     }
     48 }
     49 
     50 impl egui::Widget for &mut NoteContents<'_> {
     51     fn ui(self, ui: &mut egui::Ui) -> egui::Response {
     52         let result = render_note_contents(
     53             ui,
     54             self.ndb,
     55             self.img_cache,
     56             self.note_cache,
     57             self.txn,
     58             self.note,
     59             self.note_key,
     60             self.options,
     61         );
     62         self.action = result.action;
     63         result.response
     64     }
     65 }
     66 
     67 /// Render an inline note preview with a border. These are used when
     68 /// notes are references within a note
     69 pub fn render_note_preview(
     70     ui: &mut egui::Ui,
     71     ndb: &Ndb,
     72     note_cache: &mut NoteCache,
     73     img_cache: &mut ImageCache,
     74     txn: &Transaction,
     75     id: &[u8; 32],
     76     _id_str: &str,
     77 ) -> NoteResponse {
     78     #[cfg(feature = "profiling")]
     79     puffin::profile_function!();
     80 
     81     let note = if let Ok(note) = ndb.get_note_by_id(txn, id) {
     82         // TODO: support other preview kinds
     83         if note.kind() == 1 {
     84             note
     85         } else {
     86             return NoteResponse::new(ui.colored_label(
     87                 Color32::RED,
     88                 format!("TODO: can't preview kind {}", note.kind()),
     89             ));
     90         }
     91     } else {
     92         return NoteResponse::new(ui.colored_label(Color32::RED, "TODO: COULD NOT LOAD"));
     93         /*
     94         return ui
     95             .horizontal(|ui| {
     96                 ui.spacing_mut().item_spacing.x = 0.0;
     97                 ui.colored_label(colors::PURPLE, "@");
     98                 ui.colored_label(colors::PURPLE, &id_str[4..16]);
     99             })
    100             .response;
    101             */
    102     };
    103 
    104     egui::Frame::none()
    105         .fill(ui.visuals().noninteractive().weak_bg_fill)
    106         .inner_margin(egui::Margin::same(8.0))
    107         .outer_margin(egui::Margin::symmetric(0.0, 8.0))
    108         .rounding(egui::Rounding::same(10.0))
    109         .stroke(egui::Stroke::new(
    110             1.0,
    111             ui.visuals().noninteractive().bg_stroke.color,
    112         ))
    113         .show(ui, |ui| {
    114             ui::NoteView::new(ndb, note_cache, img_cache, &note)
    115                 .actionbar(false)
    116                 .small_pfp(true)
    117                 .wide(true)
    118                 .note_previews(false)
    119                 .options_button(true)
    120                 .show(ui)
    121         })
    122         .inner
    123 }
    124 
    125 #[allow(clippy::too_many_arguments)]
    126 fn render_note_contents(
    127     ui: &mut egui::Ui,
    128     ndb: &Ndb,
    129     img_cache: &mut ImageCache,
    130     note_cache: &mut NoteCache,
    131     txn: &Transaction,
    132     note: &Note,
    133     note_key: NoteKey,
    134     options: NoteOptions,
    135 ) -> NoteResponse {
    136     #[cfg(feature = "profiling")]
    137     puffin::profile_function!();
    138 
    139     let selectable = options.has_selectable_text();
    140     let mut images: Vec<String> = vec![];
    141     let mut inline_note: Option<(&[u8; 32], &str)> = None;
    142 
    143     let response = ui.horizontal_wrapped(|ui| {
    144         let blocks = if let Ok(blocks) = ndb.get_blocks_by_key(txn, note_key) {
    145             blocks
    146         } else {
    147             warn!("missing note content blocks? '{}'", note.content());
    148             ui.weak(note.content());
    149             return;
    150         };
    151 
    152         ui.spacing_mut().item_spacing.x = 0.0;
    153 
    154         for block in blocks.iter(note) {
    155             match block.blocktype() {
    156                 BlockType::MentionBech32 => match block.as_mention().unwrap() {
    157                     Mention::Profile(profile) => {
    158                         ui.add(ui::Mention::new(ndb, img_cache, txn, profile.pubkey()));
    159                     }
    160 
    161                     Mention::Pubkey(npub) => {
    162                         ui.add(ui::Mention::new(ndb, img_cache, txn, npub.pubkey()));
    163                     }
    164 
    165                     Mention::Note(note) if options.has_note_previews() => {
    166                         inline_note = Some((note.id(), block.as_str()));
    167                     }
    168 
    169                     Mention::Event(note) if options.has_note_previews() => {
    170                         inline_note = Some((note.id(), block.as_str()));
    171                     }
    172 
    173                     _ => {
    174                         ui.colored_label(colors::PURPLE, format!("@{}", &block.as_str()[4..16]));
    175                     }
    176                 },
    177 
    178                 BlockType::Hashtag => {
    179                     #[cfg(feature = "profiling")]
    180                     puffin::profile_scope!("hashtag contents");
    181                     ui.colored_label(colors::PURPLE, format!("#{}", block.as_str()));
    182                 }
    183 
    184                 BlockType::Url => {
    185                     let lower_url = block.as_str().to_lowercase();
    186                     if lower_url.ends_with("png") || lower_url.ends_with("jpg") {
    187                         images.push(block.as_str().to_string());
    188                     } else {
    189                         #[cfg(feature = "profiling")]
    190                         puffin::profile_scope!("url contents");
    191                         ui.add(Hyperlink::from_label_and_url(
    192                             RichText::new(block.as_str()).color(colors::PURPLE),
    193                             block.as_str(),
    194                         ));
    195                     }
    196                 }
    197 
    198                 BlockType::Text => {
    199                     #[cfg(feature = "profiling")]
    200                     puffin::profile_scope!("text contents");
    201                     ui.add(egui::Label::new(block.as_str()).selectable(selectable));
    202                 }
    203 
    204                 _ => {
    205                     ui.colored_label(colors::PURPLE, block.as_str());
    206                 }
    207             }
    208         }
    209     });
    210 
    211     let note_action = if let Some((id, block_str)) = inline_note {
    212         render_note_preview(ui, ndb, note_cache, img_cache, txn, id, block_str).action
    213     } else {
    214         NoteActionResponse::default()
    215     };
    216 
    217     if !images.is_empty() && !options.has_textmode() {
    218         ui.add_space(2.0);
    219         let carousel_id = egui::Id::new(("carousel", note.key().expect("expected tx note")));
    220         image_carousel(ui, img_cache, images, carousel_id);
    221         ui.add_space(2.0);
    222     }
    223 
    224     NoteResponse::new(response.response).with_action(note_action)
    225 }
    226 
    227 fn image_carousel(
    228     ui: &mut egui::Ui,
    229     img_cache: &mut ImageCache,
    230     images: Vec<String>,
    231     carousel_id: egui::Id,
    232 ) {
    233     // let's make sure everything is within our area
    234 
    235     let height = 360.0;
    236     let width = ui.available_size().x;
    237     let spinsz = if height > width { width } else { height };
    238 
    239     ui.add_sized([width, height], |ui: &mut egui::Ui| {
    240         egui::ScrollArea::horizontal()
    241             .id_source(carousel_id)
    242             .show(ui, |ui| {
    243                 ui.horizontal(|ui| {
    244                     for image in images {
    245                         // If the cache is empty, initiate the fetch
    246                         let m_cached_promise = img_cache.map().get(&image);
    247                         if m_cached_promise.is_none() {
    248                             let res = crate::images::fetch_img(
    249                                 img_cache,
    250                                 ui.ctx(),
    251                                 &image,
    252                                 ImageType::Content(width.round() as u32, height.round() as u32),
    253                             );
    254                             img_cache.map_mut().insert(image.to_owned(), res);
    255                         }
    256 
    257                         // What is the state of the fetch?
    258                         match img_cache.map()[&image].ready() {
    259                             // Still waiting
    260                             None => {
    261                                 ui.allocate_space(egui::vec2(spinsz, spinsz));
    262                                 //ui.add(egui::Spinner::new().size(spinsz));
    263                             }
    264                             // Failed to fetch image!
    265                             Some(Err(_err)) => {
    266                                 // FIXME - use content-specific error instead
    267                                 let no_pfp = crate::images::fetch_img(
    268                                     img_cache,
    269                                     ui.ctx(),
    270                                     ProfilePic::no_pfp_url(),
    271                                     ImageType::Profile(128),
    272                                 );
    273                                 img_cache.map_mut().insert(image.to_owned(), no_pfp);
    274                                 // spin until next pass
    275                                 ui.allocate_space(egui::vec2(spinsz, spinsz));
    276                                 //ui.add(egui::Spinner::new().size(spinsz));
    277                             }
    278                             // Use the previously resolved image
    279                             Some(Ok(img)) => {
    280                                 let img_resp = ui.add(
    281                                     Image::new(img)
    282                                         .max_height(height)
    283                                         .rounding(5.0)
    284                                         .fit_to_original_size(1.0),
    285                                 );
    286                                 img_resp.context_menu(|ui| {
    287                                     if ui.button("Copy Link").clicked() {
    288                                         ui.ctx().copy_text(image);
    289                                         ui.close_menu();
    290                                     }
    291                                 });
    292                             }
    293                         }
    294                     }
    295                 })
    296                 .response
    297             })
    298             .inner
    299     });
    300 }