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