notedeck

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

contents.rs (10601B)


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