notedeck

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

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