notedeck

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

contents.rs (11527B)


      1 use crate::gif::{handle_repaint, retrieve_latest_texture};
      2 use crate::ui::images::render_images;
      3 use crate::ui::{
      4     self,
      5     note::{NoteOptions, NoteResponse},
      6 };
      7 use crate::{actionbar::NoteAction, images::ImageType, timeline::TimelineKind};
      8 use egui::{Color32, Hyperlink, Image, RichText};
      9 use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction};
     10 use tracing::warn;
     11 
     12 use notedeck::{supported_mime_hosted_at_url, Images, MediaCacheType, NoteCache};
     13 
     14 pub struct NoteContents<'a> {
     15     ndb: &'a Ndb,
     16     img_cache: &'a mut Images,
     17     note_cache: &'a mut NoteCache,
     18     txn: &'a Transaction,
     19     note: &'a Note<'a>,
     20     options: NoteOptions,
     21     action: Option<NoteAction>,
     22 }
     23 
     24 impl<'a> NoteContents<'a> {
     25     #[allow(clippy::too_many_arguments)]
     26     pub fn new(
     27         ndb: &'a Ndb,
     28         img_cache: &'a mut Images,
     29         note_cache: &'a mut NoteCache,
     30         txn: &'a Transaction,
     31         note: &'a Note,
     32         options: ui::note::NoteOptions,
     33     ) -> Self {
     34         NoteContents {
     35             ndb,
     36             img_cache,
     37             note_cache,
     38             txn,
     39             note,
     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.options,
     60         );
     61         self.action = result.action;
     62         result.response
     63     }
     64 }
     65 
     66 /// Render an inline note preview with a border. These are used when
     67 /// notes are references within a note
     68 #[allow(clippy::too_many_arguments)]
     69 pub fn render_note_preview(
     70     ui: &mut egui::Ui,
     71     ndb: &Ndb,
     72     note_cache: &mut NoteCache,
     73     img_cache: &mut Images,
     74     txn: &Transaction,
     75     id: &[u8; 32],
     76     parent: NoteKey,
     77     note_options: NoteOptions,
     78 ) -> NoteResponse {
     79     #[cfg(feature = "profiling")]
     80     puffin::profile_function!();
     81 
     82     let note = if let Ok(note) = ndb.get_note_by_id(txn, id) {
     83         // TODO: support other preview kinds
     84         if note.kind() == 1 {
     85             note
     86         } else {
     87             return NoteResponse::new(ui.colored_label(
     88                 Color32::RED,
     89                 format!("TODO: can't preview kind {}", note.kind()),
     90             ));
     91         }
     92     } else {
     93         return NoteResponse::new(ui.colored_label(Color32::RED, "TODO: COULD NOT LOAD"));
     94         /*
     95         return ui
     96             .horizontal(|ui| {
     97                 ui.spacing_mut().item_spacing.x = 0.0;
     98                 ui.colored_label(link_color, "@");
     99                 ui.colored_label(link_color, &id_str[4..16]);
    100             })
    101             .response;
    102             */
    103     };
    104 
    105     egui::Frame::none()
    106         .fill(ui.visuals().noninteractive().weak_bg_fill)
    107         .inner_margin(egui::Margin::same(8.0))
    108         .outer_margin(egui::Margin::symmetric(0.0, 8.0))
    109         .rounding(egui::Rounding::same(10.0))
    110         .stroke(egui::Stroke::new(
    111             1.0,
    112             ui.visuals().noninteractive().bg_stroke.color,
    113         ))
    114         .show(ui, |ui| {
    115             ui::NoteView::new(ndb, note_cache, img_cache, &note, note_options)
    116                 .actionbar(false)
    117                 .small_pfp(true)
    118                 .wide(true)
    119                 .note_previews(false)
    120                 .options_button(true)
    121                 .parent(parent)
    122                 .show(ui)
    123         })
    124         .inner
    125 }
    126 
    127 #[allow(clippy::too_many_arguments)]
    128 fn render_note_contents(
    129     ui: &mut egui::Ui,
    130     ndb: &Ndb,
    131     img_cache: &mut Images,
    132     note_cache: &mut NoteCache,
    133     txn: &Transaction,
    134     note: &Note,
    135     options: NoteOptions,
    136 ) -> NoteResponse {
    137     #[cfg(feature = "profiling")]
    138     puffin::profile_function!();
    139 
    140     let note_key = note.key().expect("todo: implement non-db notes");
    141     let selectable = options.has_selectable_text();
    142     let mut images: Vec<(String, MediaCacheType)> = vec![];
    143     let mut note_action: Option<NoteAction> = None;
    144     let mut inline_note: Option<(&[u8; 32], &str)> = None;
    145     let hide_media = options.has_hide_media();
    146     let link_color = ui.visuals().hyperlink_color;
    147 
    148     let response = ui.horizontal_wrapped(|ui| {
    149         let blocks = if let Ok(blocks) = ndb.get_blocks_by_key(txn, note_key) {
    150             blocks
    151         } else {
    152             warn!("missing note content blocks? '{}'", note.content());
    153             ui.weak(note.content());
    154             return;
    155         };
    156 
    157         ui.spacing_mut().item_spacing.x = 0.0;
    158 
    159         for block in blocks.iter(note) {
    160             match block.blocktype() {
    161                 BlockType::MentionBech32 => match block.as_mention().unwrap() {
    162                     Mention::Profile(profile) => {
    163                         let act = ui::Mention::new(ndb, img_cache, txn, profile.pubkey())
    164                             .show(ui)
    165                             .inner;
    166                         if act.is_some() {
    167                             note_action = act;
    168                         }
    169                     }
    170 
    171                     Mention::Pubkey(npub) => {
    172                         let act = ui::Mention::new(ndb, img_cache, txn, npub.pubkey())
    173                             .show(ui)
    174                             .inner;
    175                         if act.is_some() {
    176                             note_action = act;
    177                         }
    178                     }
    179 
    180                     Mention::Note(note) if options.has_note_previews() => {
    181                         inline_note = Some((note.id(), block.as_str()));
    182                     }
    183 
    184                     Mention::Event(note) if options.has_note_previews() => {
    185                         inline_note = Some((note.id(), block.as_str()));
    186                     }
    187 
    188                     _ => {
    189                         ui.colored_label(link_color, format!("@{}", &block.as_str()[4..16]));
    190                     }
    191                 },
    192 
    193                 BlockType::Hashtag => {
    194                     #[cfg(feature = "profiling")]
    195                     puffin::profile_scope!("hashtag contents");
    196                     let resp = ui.colored_label(link_color, format!("#{}", block.as_str()));
    197 
    198                     if resp.clicked() {
    199                         note_action = Some(NoteAction::OpenTimeline(TimelineKind::Hashtag(
    200                             block.as_str().to_string(),
    201                         )));
    202                     } else if resp.hovered() {
    203                         ui::show_pointer(ui);
    204                     }
    205                 }
    206 
    207                 BlockType::Url => {
    208                     let mut found_supported = || -> bool {
    209                         let url = block.as_str();
    210                         if let Some(cache_type) =
    211                             supported_mime_hosted_at_url(&mut img_cache.urls, url)
    212                         {
    213                             images.push((url.to_string(), cache_type));
    214                             true
    215                         } else {
    216                             false
    217                         }
    218                     };
    219                     if hide_media || !found_supported() {
    220                         #[cfg(feature = "profiling")]
    221                         puffin::profile_scope!("url contents");
    222                         ui.add(Hyperlink::from_label_and_url(
    223                             RichText::new(block.as_str()).color(link_color),
    224                             block.as_str(),
    225                         ));
    226                     }
    227                 }
    228 
    229                 BlockType::Text => {
    230                     #[cfg(feature = "profiling")]
    231                     puffin::profile_scope!("text contents");
    232                     if options.has_scramble_text() {
    233                         ui.add(egui::Label::new(rot13(block.as_str())).selectable(selectable));
    234                     } else {
    235                         ui.add(egui::Label::new(block.as_str()).selectable(selectable));
    236                     }
    237                 }
    238 
    239                 _ => {
    240                     ui.colored_label(link_color, block.as_str());
    241                 }
    242             }
    243         }
    244     });
    245 
    246     let preview_note_action = if let Some((id, _block_str)) = inline_note {
    247         render_note_preview(ui, ndb, note_cache, img_cache, txn, id, note_key, options).action
    248     } else {
    249         None
    250     };
    251 
    252     if !images.is_empty() && !options.has_textmode() {
    253         ui.add_space(2.0);
    254         let carousel_id = egui::Id::new(("carousel", note.key().expect("expected tx note")));
    255         image_carousel(ui, img_cache, images, carousel_id);
    256         ui.add_space(2.0);
    257     }
    258 
    259     let note_action = preview_note_action.or(note_action);
    260 
    261     NoteResponse::new(response.response).with_action(note_action)
    262 }
    263 
    264 fn rot13(input: &str) -> String {
    265     input
    266         .chars()
    267         .map(|c| {
    268             if c.is_ascii_lowercase() {
    269                 // Rotate lowercase letters
    270                 (((c as u8 - b'a' + 13) % 26) + b'a') as char
    271             } else if c.is_ascii_uppercase() {
    272                 // Rotate uppercase letters
    273                 (((c as u8 - b'A' + 13) % 26) + b'A') as char
    274             } else {
    275                 // Leave other characters unchanged
    276                 c
    277             }
    278         })
    279         .collect()
    280 }
    281 
    282 fn image_carousel(
    283     ui: &mut egui::Ui,
    284     img_cache: &mut Images,
    285     images: Vec<(String, MediaCacheType)>,
    286     carousel_id: egui::Id,
    287 ) {
    288     // let's make sure everything is within our area
    289 
    290     let height = 360.0;
    291     let width = ui.available_size().x;
    292     let spinsz = if height > width { width } else { height };
    293 
    294     ui.add_sized([width, height], |ui: &mut egui::Ui| {
    295         egui::ScrollArea::horizontal()
    296             .id_salt(carousel_id)
    297             .show(ui, |ui| {
    298                 ui.horizontal(|ui| {
    299                     for (image, cache_type) in images {
    300                         render_images(
    301                             ui,
    302                             img_cache,
    303                             &image,
    304                             ImageType::Content(width.round() as u32, height.round() as u32),
    305                             cache_type,
    306                             |ui| {
    307                                 ui.allocate_space(egui::vec2(spinsz, spinsz));
    308                             },
    309                             |ui, _| {
    310                                 ui.allocate_space(egui::vec2(spinsz, spinsz));
    311                             },
    312                             |ui, url, renderable_media, gifs| {
    313                                 let texture = handle_repaint(
    314                                     ui,
    315                                     retrieve_latest_texture(&image, gifs, renderable_media),
    316                                 );
    317                                 let img_resp = ui.add(
    318                                     Image::new(texture)
    319                                         .max_height(height)
    320                                         .rounding(5.0)
    321                                         .fit_to_original_size(1.0),
    322                                 );
    323 
    324                                 img_resp.context_menu(|ui| {
    325                                     if ui.button("Copy Link").clicked() {
    326                                         ui.ctx().copy_text(url.to_owned());
    327                                         ui.close_menu();
    328                                     }
    329                                 });
    330                             },
    331                         );
    332                     }
    333                 })
    334                 .response
    335             })
    336             .inner
    337     });
    338 }