notedeck

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

contents.rs (10545B)


      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     let hide_media = options.has_hide_media();
    143 
    144     let response = ui.horizontal_wrapped(|ui| {
    145         let blocks = if let Ok(blocks) = ndb.get_blocks_by_key(txn, note_key) {
    146             blocks
    147         } else {
    148             warn!("missing note content blocks? '{}'", note.content());
    149             ui.weak(note.content());
    150             return;
    151         };
    152 
    153         ui.spacing_mut().item_spacing.x = 0.0;
    154 
    155         for block in blocks.iter(note) {
    156             match block.blocktype() {
    157                 BlockType::MentionBech32 => match block.as_mention().unwrap() {
    158                     Mention::Profile(profile) => {
    159                         ui.add(ui::Mention::new(ndb, img_cache, txn, profile.pubkey()));
    160                     }
    161 
    162                     Mention::Pubkey(npub) => {
    163                         ui.add(ui::Mention::new(ndb, img_cache, txn, npub.pubkey()));
    164                     }
    165 
    166                     Mention::Note(note) if options.has_note_previews() => {
    167                         inline_note = Some((note.id(), block.as_str()));
    168                     }
    169 
    170                     Mention::Event(note) if options.has_note_previews() => {
    171                         inline_note = Some((note.id(), block.as_str()));
    172                     }
    173 
    174                     _ => {
    175                         ui.colored_label(colors::PURPLE, format!("@{}", &block.as_str()[4..16]));
    176                     }
    177                 },
    178 
    179                 BlockType::Hashtag => {
    180                     #[cfg(feature = "profiling")]
    181                     puffin::profile_scope!("hashtag contents");
    182                     ui.colored_label(colors::PURPLE, format!("#{}", block.as_str()));
    183                 }
    184 
    185                 BlockType::Url => {
    186                     let lower_url = block.as_str().to_lowercase();
    187                     if !hide_media && (lower_url.ends_with("png") || lower_url.ends_with("jpg")) {
    188                         images.push(block.as_str().to_string());
    189                     } else {
    190                         #[cfg(feature = "profiling")]
    191                         puffin::profile_scope!("url contents");
    192                         ui.add(Hyperlink::from_label_and_url(
    193                             RichText::new(block.as_str()).color(colors::PURPLE),
    194                             block.as_str(),
    195                         ));
    196                     }
    197                 }
    198 
    199                 BlockType::Text => {
    200                     #[cfg(feature = "profiling")]
    201                     puffin::profile_scope!("text contents");
    202                     ui.add(egui::Label::new(block.as_str()).selectable(selectable));
    203                 }
    204 
    205                 _ => {
    206                     ui.colored_label(colors::PURPLE, block.as_str());
    207                 }
    208             }
    209         }
    210     });
    211 
    212     let note_action = if let Some((id, block_str)) = inline_note {
    213         render_note_preview(ui, ndb, note_cache, img_cache, txn, id, block_str).action
    214     } else {
    215         NoteActionResponse::default()
    216     };
    217 
    218     if !images.is_empty() && !options.has_textmode() {
    219         ui.add_space(2.0);
    220         let carousel_id = egui::Id::new(("carousel", note.key().expect("expected tx note")));
    221         image_carousel(ui, img_cache, images, carousel_id);
    222         ui.add_space(2.0);
    223     }
    224 
    225     NoteResponse::new(response.response).with_action(note_action)
    226 }
    227 
    228 fn image_carousel(
    229     ui: &mut egui::Ui,
    230     img_cache: &mut ImageCache,
    231     images: Vec<String>,
    232     carousel_id: egui::Id,
    233 ) {
    234     // let's make sure everything is within our area
    235 
    236     let height = 360.0;
    237     let width = ui.available_size().x;
    238     let spinsz = if height > width { width } else { height };
    239 
    240     ui.add_sized([width, height], |ui: &mut egui::Ui| {
    241         egui::ScrollArea::horizontal()
    242             .id_source(carousel_id)
    243             .show(ui, |ui| {
    244                 ui.horizontal(|ui| {
    245                     for image in images {
    246                         // If the cache is empty, initiate the fetch
    247                         let m_cached_promise = img_cache.map().get(&image);
    248                         if m_cached_promise.is_none() {
    249                             let res = crate::images::fetch_img(
    250                                 img_cache,
    251                                 ui.ctx(),
    252                                 &image,
    253                                 ImageType::Content(width.round() as u32, height.round() as u32),
    254                             );
    255                             img_cache.map_mut().insert(image.to_owned(), res);
    256                         }
    257 
    258                         // What is the state of the fetch?
    259                         match img_cache.map()[&image].ready() {
    260                             // Still waiting
    261                             None => {
    262                                 ui.allocate_space(egui::vec2(spinsz, spinsz));
    263                                 //ui.add(egui::Spinner::new().size(spinsz));
    264                             }
    265                             // Failed to fetch image!
    266                             Some(Err(_err)) => {
    267                                 // FIXME - use content-specific error instead
    268                                 let no_pfp = crate::images::fetch_img(
    269                                     img_cache,
    270                                     ui.ctx(),
    271                                     ProfilePic::no_pfp_url(),
    272                                     ImageType::Profile(128),
    273                                 );
    274                                 img_cache.map_mut().insert(image.to_owned(), no_pfp);
    275                                 // spin until next pass
    276                                 ui.allocate_space(egui::vec2(spinsz, spinsz));
    277                                 //ui.add(egui::Spinner::new().size(spinsz));
    278                             }
    279                             // Use the previously resolved image
    280                             Some(Ok(img)) => {
    281                                 let img_resp = ui.add(
    282                                     Image::new(img)
    283                                         .max_height(height)
    284                                         .rounding(5.0)
    285                                         .fit_to_original_size(1.0),
    286                                 );
    287                                 img_resp.context_menu(|ui| {
    288                                     if ui.button("Copy Link").clicked() {
    289                                         ui.ctx().copy_text(image);
    290                                         ui.close_menu();
    291                                     }
    292                                 });
    293                             }
    294                         }
    295                     }
    296                 })
    297                 .response
    298             })
    299             .inner
    300     });
    301 }