reply_description.rs (12542B)
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, JobsCache, 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 jobs: &mut JobsCache, 110 size: f32, 111 selectable: bool, 112 ) -> Option<NoteAction> { 113 let mut note_action: Option<NoteAction> = None; 114 let visuals = ui.visuals(); 115 let color = visuals.noninteractive().fg_stroke.color; 116 let link_color = visuals.hyperlink_color; 117 118 for segment in segments { 119 match segment { 120 TextSegment::Plain(text) => { 121 ui.add( 122 Label::new(RichText::new(text).size(size).color(color)).selectable(selectable), 123 ); 124 } 125 TextSegment::UserMention(pubkey) | TextSegment::ThreadUserMention(pubkey) => { 126 let action = Mention::new( 127 note_context.ndb, 128 note_context.img_cache, 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, jobs) 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, jobs) 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 jobs: &mut JobsCache, 223 ) -> Option<NoteAction> { 224 let size = 10.0; 225 let selectable = false; 226 227 let reply = note_reply.reply()?; 228 229 let reply_note = if let Ok(reply_note) = note_context.ndb.get_note_by_id(txn, reply.id) { 230 reply_note 231 } else { 232 // Handle case where reply note is not found 233 let template = tr!( 234 note_context.i18n, 235 "replying to a note", 236 "Fallback text when reply note is not found" 237 ); 238 let segments = parse_i18n_template(&template); 239 return render_text_segments( 240 ui, 241 &segments, 242 txn, 243 note_context, 244 note_options, 245 jobs, 246 size, 247 selectable, 248 ); 249 }; 250 251 if note_reply.is_reply_to_root() { 252 // Template: "replying to {user}'s {thread}" 253 let template = tr!( 254 note_context.i18n, 255 "replying to {user}'s {thread}", 256 "Template for replying to root thread", 257 user = "{user}", 258 thread = "{thread}" 259 ); 260 let mut segments = parse_i18n_template(&template); 261 fill_template_data( 262 &mut segments, 263 reply_note.pubkey(), 264 reply.id, 265 None, 266 Some(reply.id), 267 ); 268 render_text_segments( 269 ui, 270 &segments, 271 txn, 272 note_context, 273 note_options, 274 jobs, 275 size, 276 selectable, 277 ) 278 } else if let Some(root) = note_reply.root() { 279 if let Ok(root_note) = note_context.ndb.get_note_by_id(txn, root.id) { 280 if root_note.pubkey() == reply_note.pubkey() { 281 // Template: "replying to {user}'s {note}" 282 let template = tr!( 283 note_context.i18n, 284 "replying to {user}'s {note}", 285 "Template for replying to user's note", 286 user = "{user}", 287 note = "{note}" 288 ); 289 let mut segments = parse_i18n_template(&template); 290 fill_template_data(&mut segments, reply_note.pubkey(), reply.id, None, None); 291 render_text_segments( 292 ui, 293 &segments, 294 txn, 295 note_context, 296 note_options, 297 jobs, 298 size, 299 selectable, 300 ) 301 } else { 302 // Template: "replying to {reply_user}'s {note} in {thread_user}'s {thread}" 303 // This would need more sophisticated placeholder handling 304 let template = tr!( 305 note_context.i18n, 306 "replying to {user}'s {note} in {thread_user}'s {thread}", 307 "Template for replying to note in different user's thread", 308 user = "{user}", 309 note = "{note}", 310 thread_user = "{thread_user}", 311 thread = "{thread}" 312 ); 313 let mut segments = parse_i18n_template(&template); 314 fill_template_data( 315 &mut segments, 316 reply_note.pubkey(), 317 reply.id, 318 Some(root_note.pubkey()), 319 Some(root.id), 320 ); 321 render_text_segments( 322 ui, 323 &segments, 324 txn, 325 note_context, 326 note_options, 327 jobs, 328 size, 329 selectable, 330 ) 331 } 332 } else { 333 // Template: "replying to {user} in someone's thread" 334 let template = tr!( 335 note_context.i18n, 336 "replying to {user} in someone's thread", 337 "Template for replying to user in unknown thread", 338 user = "{user}" 339 ); 340 let mut segments = parse_i18n_template(&template); 341 fill_template_data(&mut segments, reply_note.pubkey(), reply.id, None, None); 342 render_text_segments( 343 ui, 344 &segments, 345 txn, 346 note_context, 347 note_options, 348 jobs, 349 size, 350 selectable, 351 ) 352 } 353 } else { 354 // Fallback 355 let template = tr!( 356 note_context.i18n, 357 "replying to {user}", 358 "Fallback template for replying to user", 359 user = "{user}" 360 ); 361 let mut segments = parse_i18n_template(&template); 362 fill_template_data(&mut segments, reply_note.pubkey(), reply.id, None, None); 363 render_text_segments( 364 ui, 365 &segments, 366 txn, 367 note_context, 368 note_options, 369 jobs, 370 size, 371 selectable, 372 ) 373 } 374 }