notedeck

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

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, &note, 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, &note, 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 }