reply_description.rs (10912B)
1 use egui::{Label, RichText, Sense}; 2 use nostrdb::{NoteReply, Transaction}; 3 4 use super::NoteOptions; 5 use crate::{jobs::JobsCache, 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 { 11 Plain(String), 12 UserMention([u8; 32]), // pubkey 13 ThreadUserMention([u8; 32]), // pubkey 14 NoteLink([u8; 32]), 15 ThreadLink([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([0; 32])), 45 "thread_user" => segments.push(TextSegment::ThreadUserMention([0; 32])), 46 "note" => segments.push(TextSegment::NoteLink([0; 32])), 47 "thread" => segments.push(TextSegment::ThreadLink([0; 32])), 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( 68 mut segments: Vec<TextSegment>, 69 reply_pubkey: &[u8; 32], 70 reply_note_id: &[u8; 32], 71 root_pubkey: Option<&[u8; 32]>, 72 root_note_id: Option<&[u8; 32]>, 73 ) -> Vec<TextSegment> { 74 for segment in &mut segments { 75 match segment { 76 TextSegment::UserMention(pubkey) if *pubkey == [0; 32] => { 77 *pubkey = *reply_pubkey; 78 } 79 TextSegment::ThreadUserMention(pubkey) if *pubkey == [0; 32] => { 80 *pubkey = *root_pubkey.unwrap_or(reply_pubkey); 81 } 82 TextSegment::NoteLink(note_id) if *note_id == [0; 32] => { 83 *note_id = *reply_note_id; 84 } 85 TextSegment::ThreadLink(note_id) if *note_id == [0; 32] => { 86 *note_id = *root_note_id.unwrap_or(reply_note_id); 87 } 88 _ => {} 89 } 90 } 91 92 segments 93 } 94 95 // Main rendering function for text segments 96 #[allow(clippy::too_many_arguments)] 97 fn render_text_segments( 98 ui: &mut egui::Ui, 99 segments: &[TextSegment], 100 txn: &Transaction, 101 note_context: &mut NoteContext, 102 note_options: NoteOptions, 103 jobs: &mut JobsCache, 104 size: f32, 105 selectable: bool, 106 ) -> Option<NoteAction> { 107 let mut note_action: Option<NoteAction> = None; 108 let visuals = ui.visuals(); 109 let color = visuals.noninteractive().fg_stroke.color; 110 let link_color = visuals.hyperlink_color; 111 112 for segment in segments { 113 match segment { 114 TextSegment::Plain(text) => { 115 ui.add( 116 Label::new(RichText::new(text).size(size).color(color)).selectable(selectable), 117 ); 118 } 119 TextSegment::UserMention(pubkey) | TextSegment::ThreadUserMention(pubkey) => { 120 let action = Mention::new(note_context.ndb, note_context.img_cache, txn, pubkey) 121 .size(size) 122 .selectable(selectable) 123 .show(ui); 124 125 if action.is_some() { 126 note_action = action; 127 } 128 } 129 TextSegment::NoteLink(note_id) => { 130 if let Ok(note) = note_context.ndb.get_note_by_id(txn, note_id) { 131 let r = ui.add( 132 Label::new( 133 RichText::new(tr!( 134 note_context.i18n, 135 "note", 136 "Link text for note references" 137 )) 138 .size(size) 139 .color(link_color), 140 ) 141 .sense(Sense::click()) 142 .selectable(selectable), 143 ); 144 145 if r.clicked() { 146 // TODO: jump to note 147 } 148 149 if r.hovered() { 150 r.on_hover_ui_at_pointer(|ui| { 151 ui.set_max_width(400.0); 152 NoteView::new(note_context, ¬e, note_options, jobs) 153 .actionbar(false) 154 .wide(true) 155 .show(ui); 156 }); 157 } 158 } 159 } 160 TextSegment::ThreadLink(note_id) => { 161 if let Ok(note) = note_context.ndb.get_note_by_id(txn, note_id) { 162 let r = ui.add( 163 Label::new( 164 RichText::new(tr!( 165 note_context.i18n, 166 "thread", 167 "Link text for thread references" 168 )) 169 .size(size) 170 .color(link_color), 171 ) 172 .sense(Sense::click()) 173 .selectable(selectable), 174 ); 175 176 if r.clicked() { 177 // TODO: jump to note 178 } 179 180 if r.hovered() { 181 r.on_hover_ui_at_pointer(|ui| { 182 ui.set_max_width(400.0); 183 NoteView::new(note_context, ¬e, note_options, jobs) 184 .actionbar(false) 185 .wide(true) 186 .show(ui); 187 }); 188 } 189 } 190 } 191 } 192 } 193 194 note_action 195 } 196 197 #[must_use = "Please handle the resulting note action"] 198 #[profiling::function] 199 pub fn reply_desc( 200 ui: &mut egui::Ui, 201 txn: &Transaction, 202 note_reply: &NoteReply, 203 note_context: &mut NoteContext, 204 note_options: NoteOptions, 205 jobs: &mut JobsCache, 206 ) -> Option<NoteAction> { 207 let size = 10.0; 208 let selectable = false; 209 210 let reply = note_reply.reply()?; 211 212 let reply_note = if let Ok(reply_note) = note_context.ndb.get_note_by_id(txn, reply.id) { 213 reply_note 214 } else { 215 // Handle case where reply note is not found 216 let template = tr!( 217 note_context.i18n, 218 "replying to a note", 219 "Fallback text when reply note is not found" 220 ); 221 let segments = parse_i18n_template(&template); 222 return render_text_segments( 223 ui, 224 &segments, 225 txn, 226 note_context, 227 note_options, 228 jobs, 229 size, 230 selectable, 231 ); 232 }; 233 234 let segments = if note_reply.is_reply_to_root() { 235 // Template: "replying to {user}'s {thread}" 236 let template = tr!( 237 note_context.i18n, 238 "replying to {user}'s {thread}", 239 "Template for replying to root thread", 240 user = "{user}", 241 thread = "{thread}" 242 ); 243 let segments = parse_i18n_template(&template); 244 fill_template_data( 245 segments, 246 reply_note.pubkey(), 247 reply.id, 248 None, 249 Some(reply.id), 250 ) 251 } else if let Some(root) = note_reply.root() { 252 if let Ok(root_note) = note_context.ndb.get_note_by_id(txn, root.id) { 253 if root_note.pubkey() == reply_note.pubkey() { 254 // Template: "replying to {user}'s {note}" 255 let template = tr!( 256 note_context.i18n, 257 "replying to {user}'s {note}", 258 "Template for replying to user's note", 259 user = "{user}", 260 note = "{note}" 261 ); 262 let segments = parse_i18n_template(&template); 263 fill_template_data(segments, reply_note.pubkey(), reply.id, None, None) 264 } else { 265 // Template: "replying to {reply_user}'s {note} in {thread_user}'s {thread}" 266 // This would need more sophisticated placeholder handling 267 let template = tr!( 268 note_context.i18n, 269 "replying to {user}'s {note} in {thread_user}'s {thread}", 270 "Template for replying to note in different user's thread", 271 user = "{user}", 272 note = "{note}", 273 thread_user = "{thread_user}", 274 thread = "{thread}" 275 ); 276 let segments = parse_i18n_template(&template); 277 fill_template_data( 278 segments, 279 reply_note.pubkey(), 280 reply.id, 281 Some(root_note.pubkey()), 282 Some(root.id), 283 ) 284 } 285 } else { 286 // Template: "replying to {user} in someone's thread" 287 let template = tr!( 288 note_context.i18n, 289 "replying to {user} in someone's thread", 290 "Template for replying to user in unknown thread", 291 user = "{user}" 292 ); 293 let segments = parse_i18n_template(&template); 294 fill_template_data(segments, reply_note.pubkey(), reply.id, None, None) 295 } 296 } else { 297 // Fallback 298 let template = tr!( 299 note_context.i18n, 300 "replying to {user}", 301 "Fallback template for replying to user", 302 user = "{user}" 303 ); 304 let segments = parse_i18n_template(&template); 305 fill_template_data(segments, reply_note.pubkey(), reply.id, None, None) 306 }; 307 308 render_text_segments( 309 ui, 310 &segments, 311 txn, 312 note_context, 313 note_options, 314 jobs, 315 size, 316 selectable, 317 ) 318 }