reply_description.rs (12379B)
1 use egui::{Label, RichText, Sense}; 2 use nostrdb::{NoteReply, Transaction}; 3 4 use super::NoteOptions; 5 use crate::{note::NoteView, Mention}; 6 use notedeck::{tr, NoteAction, NoteContext}; 7 8 // Rich text segment types for internationalized rendering 9 #[derive(Debug, Clone)] 10 pub enum TextSegment<'a> { 11 Plain(String), 12 UserMention(Option<&'a [u8; 32]>), // pubkey 13 ThreadUserMention(Option<&'a [u8; 32]>), // pubkey 14 NoteLink(Option<&'a [u8; 32]>), 15 ThreadLink(Option<&'a [u8; 32]>), 16 } 17 18 // Helper function to parse i18n template strings with placeholders 19 fn parse_i18n_template(template: &str) -> Vec<TextSegment<'_>> { 20 let mut segments = Vec::new(); 21 let mut current_text = String::new(); 22 let mut chars = template.chars().peekable(); 23 24 while let Some(ch) = chars.next() { 25 if ch == '{' { 26 // Save any accumulated plain text 27 if !current_text.is_empty() { 28 segments.push(TextSegment::Plain(current_text.clone())); 29 current_text.clear(); 30 } 31 32 // Parse placeholder 33 let mut placeholder = String::new(); 34 for ch in chars.by_ref() { 35 if ch == '}' { 36 break; 37 } 38 placeholder.push(ch); 39 } 40 41 // Handle different placeholder types 42 match placeholder.as_str() { 43 // Placeholder values will be filled later. 44 "user" => segments.push(TextSegment::UserMention(None)), 45 "thread_user" => segments.push(TextSegment::ThreadUserMention(None)), 46 "note" => segments.push(TextSegment::NoteLink(None)), 47 "thread" => segments.push(TextSegment::ThreadLink(None)), 48 _ => { 49 // Unknown placeholder, treat as plain text 50 current_text.push_str(&format!("{{{placeholder}}}")); 51 } 52 } 53 } else { 54 current_text.push(ch); 55 } 56 } 57 58 // Add any remaining plain text 59 if !current_text.is_empty() { 60 segments.push(TextSegment::Plain(current_text)); 61 } 62 63 segments 64 } 65 66 // Helper function to fill in the actual data for placeholders 67 fn fill_template_data<'a>( 68 segments: &mut [TextSegment<'a>], 69 reply_pubkey: &'a [u8; 32], 70 reply_note_id: &'a [u8; 32], 71 root_pubkey: Option<&'a [u8; 32]>, 72 root_note_id: Option<&'a [u8; 32]>, 73 ) { 74 for segment in segments { 75 match segment { 76 TextSegment::UserMention(pubkey) => { 77 if pubkey.is_none() { 78 *pubkey = Some(reply_pubkey); 79 } 80 } 81 TextSegment::ThreadUserMention(pubkey) => { 82 if pubkey.is_none() { 83 *pubkey = Some(root_pubkey.unwrap_or(reply_pubkey)); 84 } 85 } 86 TextSegment::NoteLink(note_id) => { 87 if note_id.is_none() { 88 *note_id = Some(reply_note_id); 89 } 90 } 91 TextSegment::ThreadLink(note_id) => { 92 if note_id.is_none() { 93 *note_id = Some(root_note_id.unwrap_or(reply_note_id)); 94 } 95 } 96 TextSegment::Plain(_) => {} 97 } 98 } 99 } 100 101 // Main rendering function for text segments 102 #[allow(clippy::too_many_arguments)] 103 fn render_text_segments( 104 ui: &mut egui::Ui, 105 segments: &[TextSegment<'_>], 106 txn: &Transaction, 107 note_context: &mut NoteContext, 108 note_options: NoteOptions, 109 size: f32, 110 selectable: bool, 111 ) -> Option<NoteAction> { 112 let mut note_action: Option<NoteAction> = None; 113 let visuals = ui.visuals(); 114 let color = visuals.noninteractive().fg_stroke.color; 115 let link_color = visuals.hyperlink_color; 116 117 for segment in segments { 118 match &segment { 119 TextSegment::Plain(text) => { 120 ui.add( 121 Label::new(RichText::new(text).size(size).color(color)).selectable(selectable), 122 ); 123 } 124 TextSegment::UserMention(pubkey) | TextSegment::ThreadUserMention(pubkey) => { 125 let action = Mention::new( 126 note_context.ndb, 127 note_context.img_cache, 128 note_context.jobs, 129 txn, 130 pubkey.expect("expected pubkey"), 131 ) 132 .size(size) 133 .selectable(selectable) 134 .show(ui); 135 136 if action.is_some() { 137 note_action = action; 138 } 139 } 140 TextSegment::NoteLink(note_id) => { 141 if let Ok(note) = note_context 142 .ndb 143 .get_note_by_id(txn, note_id.expect("expected text segment note_id")) 144 { 145 let r = ui.add( 146 Label::new( 147 RichText::new(tr!( 148 note_context.i18n, 149 "note", 150 "Link text for note references" 151 )) 152 .size(size) 153 .color(link_color), 154 ) 155 .sense(Sense::click()) 156 .selectable(selectable), 157 ); 158 159 if r.clicked() { 160 // TODO: jump to note 161 } 162 163 if r.hovered() { 164 r.on_hover_ui_at_pointer(|ui| { 165 ui.set_max_width(400.0); 166 NoteView::new(note_context, ¬e, note_options) 167 .actionbar(false) 168 .wide(true) 169 .show(ui); 170 }); 171 } 172 } 173 } 174 TextSegment::ThreadLink(note_id) => { 175 if let Ok(note) = note_context 176 .ndb 177 .get_note_by_id(txn, note_id.expect("expected text segment threadlink")) 178 { 179 let r = ui.add( 180 Label::new( 181 RichText::new(tr!( 182 note_context.i18n, 183 "thread", 184 "Link text for thread references" 185 )) 186 .size(size) 187 .color(link_color), 188 ) 189 .sense(Sense::click()) 190 .selectable(selectable), 191 ); 192 193 if r.clicked() { 194 // TODO: jump to note 195 } 196 197 if r.hovered() { 198 r.on_hover_ui_at_pointer(|ui| { 199 ui.set_max_width(400.0); 200 NoteView::new(note_context, ¬e, note_options) 201 .actionbar(false) 202 .wide(true) 203 .show(ui); 204 }); 205 } 206 } 207 } 208 } 209 } 210 211 note_action 212 } 213 214 #[must_use = "Please handle the resulting note action"] 215 #[profiling::function] 216 pub fn reply_desc( 217 ui: &mut egui::Ui, 218 txn: &Transaction, 219 note_reply: &NoteReply, 220 note_context: &mut NoteContext, 221 note_options: NoteOptions, 222 ) -> Option<NoteAction> { 223 let size = 10.0; 224 let selectable = false; 225 226 let reply = note_reply.reply()?; 227 228 let reply_note = if let Ok(reply_note) = note_context.ndb.get_note_by_id(txn, reply.id) { 229 reply_note 230 } else { 231 // Handle case where reply note is not found 232 let template = tr!( 233 note_context.i18n, 234 "replying to a note", 235 "Fallback text when reply note is not found" 236 ); 237 let segments = parse_i18n_template(&template); 238 return render_text_segments( 239 ui, 240 &segments, 241 txn, 242 note_context, 243 note_options, 244 size, 245 selectable, 246 ); 247 }; 248 249 if note_reply.is_reply_to_root() { 250 // Template: "replying to {user}'s {thread}" 251 let template = tr!( 252 note_context.i18n, 253 "replying to {user}'s {thread}", 254 "Template for replying to root thread", 255 user = "{user}", 256 thread = "{thread}" 257 ); 258 let mut segments = parse_i18n_template(&template); 259 fill_template_data( 260 &mut segments, 261 reply_note.pubkey(), 262 reply.id, 263 None, 264 Some(reply.id), 265 ); 266 render_text_segments( 267 ui, 268 &segments, 269 txn, 270 note_context, 271 note_options, 272 size, 273 selectable, 274 ) 275 } else if let Some(root) = note_reply.root() { 276 if let Ok(root_note) = note_context.ndb.get_note_by_id(txn, root.id) { 277 if root_note.pubkey() == reply_note.pubkey() { 278 // Template: "replying to {user}'s {note}" 279 let template = tr!( 280 note_context.i18n, 281 "replying to {user}'s {note}", 282 "Template for replying to user's note", 283 user = "{user}", 284 note = "{note}" 285 ); 286 let mut segments = parse_i18n_template(&template); 287 fill_template_data(&mut segments, reply_note.pubkey(), reply.id, None, None); 288 render_text_segments( 289 ui, 290 &segments, 291 txn, 292 note_context, 293 note_options, 294 size, 295 selectable, 296 ) 297 } else { 298 // Template: "replying to {reply_user}'s {note} in {thread_user}'s {thread}" 299 // This would need more sophisticated placeholder handling 300 let template = tr!( 301 note_context.i18n, 302 "replying to {user}'s {note} in {thread_user}'s {thread}", 303 "Template for replying to note in different user's thread", 304 user = "{user}", 305 note = "{note}", 306 thread_user = "{thread_user}", 307 thread = "{thread}" 308 ); 309 let mut segments = parse_i18n_template(&template); 310 fill_template_data( 311 &mut segments, 312 reply_note.pubkey(), 313 reply.id, 314 Some(root_note.pubkey()), 315 Some(root.id), 316 ); 317 render_text_segments( 318 ui, 319 &segments, 320 txn, 321 note_context, 322 note_options, 323 size, 324 selectable, 325 ) 326 } 327 } else { 328 // Template: "replying to {user} in someone's thread" 329 let template = tr!( 330 note_context.i18n, 331 "replying to {user} in someone's thread", 332 "Template for replying to user in unknown thread", 333 user = "{user}" 334 ); 335 let mut segments = parse_i18n_template(&template); 336 fill_template_data(&mut segments, reply_note.pubkey(), reply.id, None, None); 337 render_text_segments( 338 ui, 339 &segments, 340 txn, 341 note_context, 342 note_options, 343 size, 344 selectable, 345 ) 346 } 347 } else { 348 // Fallback 349 let template = tr!( 350 note_context.i18n, 351 "replying to {user}", 352 "Fallback template for replying to user", 353 user = "{user}" 354 ); 355 let mut segments = parse_i18n_template(&template); 356 fill_template_data(&mut segments, reply_note.pubkey(), reply.id, None, None); 357 render_text_segments( 358 ui, 359 &segments, 360 txn, 361 note_context, 362 note_options, 363 size, 364 selectable, 365 ) 366 } 367 }