notedeck

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

post.rs (45059B)


      1 use egui::{
      2     text::{CCursor, CCursorRange, LayoutJob},
      3     text_edit::TextEditOutput,
      4     TextBuffer, TextEdit, TextFormat,
      5 };
      6 use enostr::{FullKeypair, Pubkey};
      7 use nostrdb::{Note, NoteBuilder, NoteReply};
      8 use std::{
      9     any::TypeId,
     10     collections::{BTreeMap, HashMap, HashSet},
     11     hash::{DefaultHasher, Hash, Hasher},
     12     ops::Range,
     13 };
     14 use tracing::error;
     15 
     16 use crate::media_upload::Nip94Event;
     17 
     18 pub struct NewPost {
     19     pub content: String,
     20     pub account: FullKeypair,
     21     pub media: Vec<Nip94Event>,
     22     pub mentions: Vec<Pubkey>,
     23 }
     24 
     25 fn client_variant() -> &'static str {
     26     #[cfg(target_os = "android")]
     27     {
     28         "Damus Android"
     29     }
     30 
     31     #[cfg(not(target_os = "android"))]
     32     {
     33         "Damus Notedeck"
     34     }
     35 }
     36 
     37 fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> {
     38     builder
     39         .start_tag()
     40         .tag_str("client")
     41         .tag_str(client_variant())
     42 }
     43 
     44 impl NewPost {
     45     pub fn new(
     46         content: String,
     47         account: enostr::FullKeypair,
     48         media: Vec<Nip94Event>,
     49         mentions: Vec<Pubkey>,
     50     ) -> Self {
     51         NewPost {
     52             content,
     53             account,
     54             media,
     55             mentions,
     56         }
     57     }
     58 
     59     /// creates a NoteBuilder with all the shared data between note, reply & quote reply
     60     fn builder_with_shared_tags<'a>(&self, mut content: String) -> NoteBuilder<'a> {
     61         append_urls(&mut content, &self.media);
     62 
     63         let mut builder = NoteBuilder::new().kind(1).content(&content);
     64         builder = add_client_tag(builder);
     65 
     66         for hashtag in Self::extract_hashtags(&self.content) {
     67             builder = builder.start_tag().tag_str("t").tag_str(&hashtag);
     68         }
     69 
     70         if !self.media.is_empty() {
     71             builder = add_imeta_tags(builder, &self.media);
     72         }
     73 
     74         if !self.mentions.is_empty() {
     75             builder = add_mention_tags(builder, &self.mentions);
     76         }
     77 
     78         builder
     79     }
     80 
     81     pub fn to_note(&self, seckey: &[u8; 32]) -> Note<'_> {
     82         let builder = self.builder_with_shared_tags(self.content.clone());
     83 
     84         builder.sign(seckey).build().expect("note should be ok")
     85     }
     86 
     87     pub fn to_reply(&self, seckey: &[u8; 32], replying_to: &Note) -> Note<'_> {
     88         let mut builder = self.builder_with_shared_tags(self.content.clone());
     89 
     90         let nip10 = NoteReply::new(replying_to.tags());
     91 
     92         builder = if let Some(root) = nip10.root() {
     93             builder
     94                 .start_tag()
     95                 .tag_str("e")
     96                 .tag_str(&hex::encode(root.id))
     97                 .tag_str("")
     98                 .tag_str("root")
     99                 .start_tag()
    100                 .tag_str("e")
    101                 .tag_str(&hex::encode(replying_to.id()))
    102                 .tag_str("")
    103                 .tag_str("reply")
    104                 .sign(seckey)
    105         } else {
    106             // we're replying to a post that isn't in a thread,
    107             // just add a single reply-to-root tag
    108             builder
    109                 .start_tag()
    110                 .tag_str("e")
    111                 .tag_str(&hex::encode(replying_to.id()))
    112                 .tag_str("")
    113                 .tag_str("root")
    114                 .sign(seckey)
    115         };
    116 
    117         let mut seen_p: HashSet<&[u8; 32]> = HashSet::new();
    118 
    119         builder = builder
    120             .start_tag()
    121             .tag_str("p")
    122             .tag_str(&hex::encode(replying_to.pubkey()));
    123 
    124         seen_p.insert(replying_to.pubkey());
    125 
    126         for tag in replying_to.tags() {
    127             if tag.count() < 2 {
    128                 continue;
    129             }
    130 
    131             if tag.get_unchecked(0).variant().str() != Some("p") {
    132                 continue;
    133             }
    134 
    135             let id = if let Some(id) = tag.get_unchecked(1).variant().id() {
    136                 id
    137             } else {
    138                 continue;
    139             };
    140 
    141             if seen_p.contains(id) {
    142                 continue;
    143             }
    144 
    145             seen_p.insert(id);
    146 
    147             builder = builder.start_tag().tag_str("p").tag_str(&hex::encode(id));
    148         }
    149 
    150         builder
    151             .sign(seckey)
    152             .build()
    153             .expect("expected build to work")
    154     }
    155 
    156     pub fn to_quote(&self, seckey: &[u8; 32], quoting: &Note) -> Note<'_> {
    157         let new_content = format!(
    158             "{}\nnostr:{}",
    159             self.content,
    160             enostr::NoteId::new(*quoting.id()).to_bech().unwrap()
    161         );
    162 
    163         let builder = self.builder_with_shared_tags(new_content);
    164 
    165         builder
    166             .start_tag()
    167             .tag_str("q")
    168             .tag_str(&hex::encode(quoting.id()))
    169             .start_tag()
    170             .tag_str("p")
    171             .tag_str(&hex::encode(quoting.pubkey()))
    172             .sign(seckey)
    173             .build()
    174             .expect("expected build to work")
    175     }
    176 
    177     fn extract_hashtags(content: &str) -> HashSet<String> {
    178         let mut hashtags = HashSet::new();
    179         for word in
    180             content.split(|c: char| c.is_whitespace() || (c.is_ascii_punctuation() && c != '#'))
    181         {
    182             if word.starts_with('#') && word.len() > 1 {
    183                 let tag = word[1..].to_lowercase();
    184                 if !tag.is_empty() {
    185                     hashtags.insert(tag);
    186                 }
    187             }
    188         }
    189         hashtags
    190     }
    191 }
    192 
    193 fn append_urls(content: &mut String, media: &Vec<Nip94Event>) {
    194     for ev in media {
    195         content.push(' ');
    196         content.push_str(&ev.url);
    197     }
    198 }
    199 
    200 fn add_mention_tags<'a>(builder: NoteBuilder<'a>, mentions: &Vec<Pubkey>) -> NoteBuilder<'a> {
    201     let mut builder = builder;
    202 
    203     for mention in mentions {
    204         builder = builder.start_tag().tag_str("p").tag_str(&mention.hex());
    205     }
    206 
    207     builder
    208 }
    209 
    210 fn add_imeta_tags<'a>(builder: NoteBuilder<'a>, media: &Vec<Nip94Event>) -> NoteBuilder<'a> {
    211     let mut builder = builder;
    212     for item in media {
    213         builder = builder
    214             .start_tag()
    215             .tag_str("imeta")
    216             .tag_str(&format!("url {}", item.url));
    217 
    218         if let Some(ox) = &item.ox {
    219             builder = builder.tag_str(&format!("ox {ox}"));
    220         };
    221         if let Some(x) = &item.x {
    222             builder = builder.tag_str(&format!("x {x}"));
    223         }
    224         if let Some(media_type) = &item.media_type {
    225             builder = builder.tag_str(&format!("m {media_type}"));
    226         }
    227         if let Some(dims) = &item.dimensions {
    228             builder = builder.tag_str(&format!("dim {}x{}", dims.0, dims.1));
    229         }
    230         if let Some(bh) = &item.blurhash {
    231             builder = builder.tag_str(&format!("blurhash {bh}"));
    232         }
    233         if let Some(thumb) = &item.thumb {
    234             builder = builder.tag_str(&format!("thumb {thumb}"));
    235         }
    236     }
    237     builder
    238 }
    239 
    240 type MentionKey = usize;
    241 
    242 #[derive(Debug, Clone)]
    243 pub struct PostBuffer {
    244     pub text_buffer: String,
    245     pub mention_indicator: char,
    246     pub mentions: HashMap<MentionKey, MentionInfo>,
    247     mentions_key: MentionKey,
    248     pub selected_mention: bool,
    249 
    250     // the start index of a mention is inclusive
    251     pub mention_starts: BTreeMap<usize, MentionKey>, // maps the mention start index with the correct `MentionKey`
    252 
    253     // the end index of a mention is exclusive
    254     pub mention_ends: BTreeMap<usize, MentionKey>, // maps the mention end index with the correct `MentionKey`
    255 }
    256 
    257 impl Default for PostBuffer {
    258     fn default() -> Self {
    259         Self {
    260             mention_indicator: '@',
    261             mentions_key: 0,
    262             selected_mention: false,
    263             text_buffer: Default::default(),
    264             mentions: Default::default(),
    265             mention_starts: Default::default(),
    266             mention_ends: Default::default(),
    267         }
    268     }
    269 }
    270 
    271 /// New cursor index (indexed by characters) after operation is performed
    272 #[must_use = "must call MentionSelectedResponse::process"]
    273 pub struct MentionSelectedResponse {
    274     pub next_cursor_index: usize,
    275 }
    276 
    277 impl MentionSelectedResponse {
    278     pub fn process(&self, ctx: &egui::Context, text_edit_output: &TextEditOutput) {
    279         let text_edit_id = text_edit_output.response.id;
    280         let Some(mut before_state) = TextEdit::load_state(ctx, text_edit_id) else {
    281             return;
    282         };
    283 
    284         before_state
    285             .cursor
    286             .set_char_range(Some(CCursorRange::one(CCursor::new(
    287                 self.next_cursor_index,
    288             ))));
    289 
    290         ctx.memory_mut(|mem| mem.request_focus(text_edit_id));
    291 
    292         TextEdit::store_state(ctx, text_edit_id, before_state);
    293     }
    294 }
    295 
    296 impl PostBuffer {
    297     pub fn get_new_mentions_key(&mut self) -> usize {
    298         let prev = self.mentions_key;
    299         self.mentions_key += 1;
    300         prev
    301     }
    302 
    303     pub fn get_mention(&self, cursor_index: usize) -> Option<MentionIndex<'_>> {
    304         self.mention_ends
    305             .range(cursor_index..)
    306             .next()
    307             .and_then(|(_, mention_key)| {
    308                 self.mentions
    309                     .get(mention_key)
    310                     .filter(|info| {
    311                         if let MentionType::Finalized(_) = info.mention_type {
    312                             // should exclude the last character if we're finalized
    313                             info.start_index <= cursor_index && cursor_index < info.end_index
    314                         } else {
    315                             info.start_index <= cursor_index && cursor_index <= info.end_index
    316                         }
    317                     })
    318                     .map(|info| MentionIndex {
    319                         index: *mention_key,
    320                         info,
    321                     })
    322             })
    323     }
    324 
    325     pub fn get_mention_string<'a>(&'a self, mention_key: &MentionIndex<'a>) -> &'a str {
    326         self.text_buffer
    327             .char_range(mention_key.info.start_index + 1..mention_key.info.end_index)
    328         // don't include the delim
    329     }
    330 
    331     pub fn select_full_mention(&mut self, mention_key: usize, pk: Pubkey) {
    332         if let Some(info) = self.mentions.get_mut(&mention_key) {
    333             info.mention_type = MentionType::Finalized(pk);
    334             self.selected_mention = true;
    335         } else {
    336             error!("Error selecting mention for index: {mention_key}. Have the following mentions: {:?}", self.mentions);
    337         }
    338     }
    339 
    340     pub fn select_mention_and_replace_name(
    341         &mut self,
    342         mention_key: usize,
    343         full_name: &str,
    344         pk: Pubkey,
    345     ) -> Option<MentionSelectedResponse> {
    346         let Some(info) = self.mentions.get(&mention_key) else {
    347             error!("Error selecting mention for index: {mention_key}. Have the following mentions: {:?}", self.mentions);
    348             return None;
    349         };
    350         let text_start_index = info.start_index + 1; // increment by one to exclude the mention indicator, '@'
    351         self.delete_char_range(text_start_index..info.end_index);
    352         let text_chars_inserted = self.insert_text(full_name, text_start_index);
    353         self.select_full_mention(mention_key, pk);
    354 
    355         let space_chars_inserted = self.insert_text(" ", text_start_index + text_chars_inserted);
    356 
    357         Some(MentionSelectedResponse {
    358             next_cursor_index: text_start_index + text_chars_inserted + space_chars_inserted,
    359         })
    360     }
    361 
    362     pub fn delete_mention(&mut self, mention_key: usize) {
    363         if let Some(mention_info) = self.mentions.get(&mention_key) {
    364             self.mention_starts.remove(&mention_info.start_index);
    365             self.mention_ends.remove(&mention_info.end_index);
    366             self.mentions.remove(&mention_key);
    367         }
    368     }
    369 
    370     pub fn is_empty(&self) -> bool {
    371         self.text_buffer.is_empty()
    372     }
    373 
    374     pub fn output(&self) -> PostOutput {
    375         let mut out = self.text_buffer.clone();
    376         let mut mentions = Vec::new();
    377         for (cur_end_ind, mention_ind) in self.mention_ends.iter().rev() {
    378             if let Some(info) = self.mentions.get(mention_ind) {
    379                 if let MentionType::Finalized(pk) = info.mention_type {
    380                     if let Some(bech) = pk.npub() {
    381                         if let Some(byte_range) =
    382                             char_indices_to_byte(&out, info.start_index..*cur_end_ind)
    383                         {
    384                             out.replace_range(byte_range, &format!("nostr:{bech}"));
    385                             mentions.push(pk);
    386                         }
    387                     }
    388                 }
    389             }
    390         }
    391         mentions.reverse();
    392 
    393         PostOutput {
    394             text: out,
    395             mentions,
    396         }
    397     }
    398 
    399     pub fn to_layout_job(&self, ui: &egui::Ui) -> LayoutJob {
    400         let mut job = LayoutJob::default();
    401         let colored_fmt = default_text_format_colored(ui, notedeck_ui::colors::PINK);
    402 
    403         let mut prev_text_char_index = 0;
    404         let mut prev_text_byte_index = 0;
    405         for (start_char_index, mention_ind) in &self.mention_starts {
    406             if let Some(info) = self.mentions.get(mention_ind) {
    407                 if matches!(info.mention_type, MentionType::Finalized(_)) {
    408                     let end_char_index = info.end_index;
    409 
    410                     let char_indices = prev_text_char_index..*start_char_index;
    411                     if let Some(byte_indicies) =
    412                         char_indices_to_byte(&self.text_buffer, char_indices.clone())
    413                     {
    414                         if let Some(prev_text) = self.text_buffer.get(byte_indicies.clone()) {
    415                             job.append(prev_text, 0.0, default_text_format(ui));
    416                             prev_text_char_index = *start_char_index;
    417                             prev_text_byte_index = byte_indicies.end;
    418                         }
    419                     }
    420 
    421                     let char_indices = *start_char_index..end_char_index;
    422                     if let Some(byte_indicies) =
    423                         char_indices_to_byte(&self.text_buffer, char_indices.clone())
    424                     {
    425                         if let Some(cur_text) = self.text_buffer.get(byte_indicies.clone()) {
    426                             job.append(cur_text, 0.0, colored_fmt.clone());
    427                             prev_text_char_index = end_char_index;
    428                             prev_text_byte_index = byte_indicies.end;
    429                         }
    430                     }
    431                 }
    432             }
    433         }
    434 
    435         if prev_text_byte_index < self.text_buffer.len() {
    436             if let Some(cur_text) = self.text_buffer.get(prev_text_byte_index..) {
    437                 job.append(cur_text, 0.0, default_text_format(ui));
    438             } else {
    439                 error!(
    440                     "could not retrieve substring from [{} to {}) in PostBuffer::text_buffer",
    441                     prev_text_byte_index,
    442                     self.text_buffer.len()
    443                 );
    444             }
    445         }
    446 
    447         job
    448     }
    449 
    450     pub fn need_new_layout(&self, cache: Option<&(String, LayoutJob)>) -> bool {
    451         if let Some((text, _)) = cache {
    452             if self.selected_mention {
    453                 return true;
    454             }
    455 
    456             self.text_buffer != *text
    457         } else {
    458             true
    459         }
    460     }
    461 }
    462 
    463 fn char_indices_to_byte(text: &str, char_range: Range<usize>) -> Option<Range<usize>> {
    464     let mut char_indices = text.char_indices();
    465 
    466     let start = char_indices.nth(char_range.start)?.0;
    467     let end = if char_range.end < text.chars().count() {
    468         char_indices.nth(char_range.end - char_range.start - 1)?.0
    469     } else {
    470         text.len()
    471     };
    472 
    473     Some(start..end)
    474 }
    475 
    476 pub fn downcast_post_buffer(buffer: &dyn TextBuffer) -> Option<&PostBuffer> {
    477     let mut hasher = DefaultHasher::new();
    478     TypeId::of::<PostBuffer>().hash(&mut hasher);
    479     let post_id = hasher.finish() as usize;
    480 
    481     if buffer.type_id() == post_id {
    482         unsafe { Some(&*(buffer as *const dyn TextBuffer as *const PostBuffer)) }
    483     } else {
    484         None
    485     }
    486 }
    487 
    488 fn default_text_format(ui: &egui::Ui) -> TextFormat {
    489     default_text_format_colored(
    490         ui,
    491         ui.visuals()
    492             .override_text_color
    493             .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color()),
    494     )
    495 }
    496 
    497 fn default_text_format_colored(ui: &egui::Ui, color: egui::Color32) -> TextFormat {
    498     TextFormat::simple(egui::FontSelection::default().resolve(ui.style()), color)
    499 }
    500 
    501 pub struct PostOutput {
    502     pub text: String,
    503     pub mentions: Vec<Pubkey>,
    504 }
    505 
    506 #[derive(Debug)]
    507 pub struct MentionIndex<'a> {
    508     pub index: usize,
    509     pub info: &'a MentionInfo,
    510 }
    511 
    512 #[derive(Clone, Debug, PartialEq)]
    513 pub enum MentionType {
    514     Pending,
    515     Finalized(Pubkey),
    516 }
    517 
    518 impl TextBuffer for PostBuffer {
    519     fn is_mutable(&self) -> bool {
    520         true
    521     }
    522 
    523     fn as_str(&self) -> &str {
    524         self.text_buffer.as_str()
    525     }
    526 
    527     fn insert_text(&mut self, text: &str, char_index: usize) -> usize {
    528         if text.is_empty() {
    529             return 0;
    530         }
    531         let text_num_chars = text.chars().count();
    532         self.text_buffer.insert_text(text, char_index);
    533 
    534         // the text was inserted before or inside these mentions. We need to at least move their ends
    535         let pending_ends_to_update: Vec<usize> = self
    536             .mention_ends
    537             .range(char_index..)
    538             .filter(|(k, v)| {
    539                 let is_last = **k == char_index;
    540                 let is_finalized = if let Some(info) = self.mentions.get(*v) {
    541                     matches!(info.mention_type, MentionType::Finalized(_))
    542                 } else {
    543                     false
    544                 };
    545                 !(is_last && is_finalized)
    546             })
    547             .map(|(&k, _)| k)
    548             .collect();
    549 
    550         let mut break_mentions = Vec::new();
    551         for cur_end in pending_ends_to_update {
    552             let mention_key = if let Some(mention_key) = self.mention_ends.get(&cur_end) {
    553                 *mention_key
    554             } else {
    555                 continue;
    556             };
    557 
    558             self.mention_ends.remove(&cur_end);
    559 
    560             let new_end = cur_end + text_num_chars;
    561             self.mention_ends.insert(new_end, mention_key);
    562             // replaced the current end with the new value
    563 
    564             if let Some(mention_info) = self.mentions.get_mut(&mention_key) {
    565                 if mention_info.start_index >= char_index {
    566                     // the text is being inserted before this mention. move the start index as well
    567                     self.mention_starts.remove(&mention_info.start_index);
    568                     let new_start = mention_info.start_index + text_num_chars;
    569                     self.mention_starts.insert(new_start, mention_key);
    570                     mention_info.start_index = new_start;
    571                 } else {
    572                     if char_index == mention_info.end_index
    573                         && first_is_desired_char(&self.text_buffer, text, char_index, ' ')
    574                     {
    575                         // if the user wrote a double space at the end of the mention, break it
    576                         break_mentions.push(mention_key);
    577                     }
    578 
    579                     // text is being inserted inside this mention. Make sure it is in the pending state
    580                     mention_info.mention_type = MentionType::Pending;
    581                 }
    582 
    583                 mention_info.end_index = new_end;
    584             } else {
    585                 error!("Could not find mention at index {}", mention_key);
    586             }
    587         }
    588 
    589         for mention_key in break_mentions {
    590             self.delete_mention(mention_key);
    591         }
    592 
    593         if first_is_desired_char(&self.text_buffer, text, char_index, self.mention_indicator) {
    594             // if a mention already exists where we're inserting the delim, remove it
    595             let to_remove = self.get_mention(char_index).map(|old_mention| {
    596                 (
    597                     old_mention.index,
    598                     old_mention.info.start_index..old_mention.info.end_index,
    599                 )
    600             });
    601 
    602             if let Some((key, range)) = to_remove {
    603                 self.mention_ends.remove(&range.end);
    604                 self.mention_starts.remove(&range.start);
    605                 self.mentions.remove(&key);
    606             }
    607 
    608             let start_index = char_index;
    609             let end_index = char_index + text_num_chars;
    610             let mention_key = self.get_new_mentions_key();
    611             self.mentions.insert(
    612                 mention_key,
    613                 MentionInfo {
    614                     start_index,
    615                     end_index,
    616                     mention_type: MentionType::Pending,
    617                 },
    618             );
    619             self.mention_starts.insert(start_index, mention_key);
    620             self.mention_ends.insert(end_index, mention_key);
    621         }
    622 
    623         text_num_chars
    624     }
    625 
    626     fn delete_char_range(&mut self, char_range: Range<usize>) {
    627         let deletion_num_chars = char_range.len();
    628         let Range {
    629             start: deletion_start,
    630             end: deletion_end,
    631         } = char_range;
    632 
    633         self.text_buffer.delete_char_range(char_range);
    634 
    635         // these mentions will be affected by the deletion
    636         let ends_to_update: Vec<usize> = self
    637             .mention_ends
    638             .range(deletion_start..)
    639             .map(|(&k, _)| k)
    640             .collect();
    641 
    642         for cur_mention_end in ends_to_update {
    643             let mention_key = match &self.mention_ends.get(&cur_mention_end) {
    644                 Some(ind) => **ind,
    645                 None => continue,
    646             };
    647             let cur_mention_start = match self.mentions.get(&mention_key) {
    648                 Some(i) => i.start_index,
    649                 None => {
    650                     error!("Could not find mention at index {}", mention_key);
    651                     continue;
    652                 }
    653             };
    654 
    655             if cur_mention_end <= deletion_start {
    656                 // nothing happens to this mention
    657                 continue;
    658             }
    659 
    660             let status = if cur_mention_start >= deletion_start {
    661                 if cur_mention_start >= deletion_end {
    662                     // mention falls after the range
    663                     // need to shift both start and end
    664 
    665                     DeletionStatus::ShiftStartAndEnd(
    666                         cur_mention_start - deletion_num_chars,
    667                         cur_mention_end - deletion_num_chars,
    668                     )
    669                 } else {
    670                     // fully delete mention
    671 
    672                     DeletionStatus::FullyRemove
    673                 }
    674             } else if cur_mention_end > deletion_end {
    675                 // inner partial delete
    676 
    677                 DeletionStatus::ShiftEnd(cur_mention_end - deletion_num_chars)
    678             } else {
    679                 // outer partial delete
    680 
    681                 DeletionStatus::ShiftEnd(deletion_start)
    682             };
    683 
    684             match status {
    685                 DeletionStatus::FullyRemove => {
    686                     self.mention_starts.remove(&cur_mention_start);
    687                     self.mention_ends.remove(&cur_mention_end);
    688                     self.mentions.remove(&mention_key);
    689                 }
    690                 DeletionStatus::ShiftEnd(new_end)
    691                 | DeletionStatus::ShiftStartAndEnd(_, new_end) => {
    692                     let mention_info = match self.mentions.get_mut(&mention_key) {
    693                         Some(i) => i,
    694                         None => {
    695                             error!("Could not find mention at index {}", mention_key);
    696                             continue;
    697                         }
    698                     };
    699 
    700                     self.mention_ends.remove(&cur_mention_end);
    701                     self.mention_ends.insert(new_end, mention_key);
    702                     mention_info.end_index = new_end;
    703 
    704                     if let DeletionStatus::ShiftStartAndEnd(new_start, _) = status {
    705                         self.mention_starts.remove(&cur_mention_start);
    706                         self.mention_starts.insert(new_start, mention_key);
    707                         mention_info.start_index = new_start;
    708                     }
    709 
    710                     if let DeletionStatus::ShiftEnd(_) = status {
    711                         mention_info.mention_type = MentionType::Pending;
    712                     }
    713                 }
    714             }
    715         }
    716     }
    717 
    718     fn type_id(&self) -> usize {
    719         let mut hasher = DefaultHasher::new();
    720         TypeId::of::<PostBuffer>().hash(&mut hasher);
    721         hasher.finish() as usize
    722     }
    723 }
    724 
    725 fn first_is_desired_char(
    726     full_text: &str,
    727     new_text: &str,
    728     new_text_index: usize,
    729     desired: char,
    730 ) -> bool {
    731     new_text.chars().next().is_some_and(|c| {
    732         c == desired
    733             && (new_text_index == 0 || full_text.chars().nth(new_text_index - 1) == Some(' '))
    734     })
    735 }
    736 
    737 #[derive(Debug)]
    738 enum DeletionStatus {
    739     FullyRemove,
    740     ShiftEnd(usize),
    741     ShiftStartAndEnd(usize, usize),
    742 }
    743 
    744 #[derive(Debug, PartialEq, Clone)]
    745 pub struct MentionInfo {
    746     pub start_index: usize,
    747     pub end_index: usize,
    748     pub mention_type: MentionType,
    749 }
    750 
    751 #[cfg(test)]
    752 mod tests {
    753     use super::*;
    754     use pretty_assertions::assert_eq;
    755 
    756     impl MentionInfo {
    757         pub fn bounds(&self) -> Range<usize> {
    758             self.start_index..self.end_index
    759         }
    760     }
    761 
    762     const JB55: fn() -> Pubkey = || {
    763         Pubkey::from_hex("32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")
    764             .unwrap()
    765     };
    766     const KK: fn() -> Pubkey = || {
    767         Pubkey::from_hex("4a0510f26880d40e432f4865cb5714d9d3c200ca6ebb16b418ae6c555f574967")
    768             .unwrap()
    769     };
    770 
    771     #[derive(PartialEq, Clone, Debug)]
    772     struct MentionExample {
    773         text: String,
    774         mention1: Option<MentionInfo>,
    775         mention2: Option<MentionInfo>,
    776         mention3: Option<MentionInfo>,
    777         mention4: Option<MentionInfo>,
    778     }
    779 
    780     fn apply_mention_example(buf: &mut PostBuffer) -> MentionExample {
    781         buf.insert_text("test ", 0);
    782         buf.insert_text("@jb55", 5);
    783         buf.select_full_mention(0, JB55());
    784         buf.insert_text(" test ", 10);
    785         buf.insert_text("@vrod", 16);
    786         buf.select_full_mention(1, JB55());
    787         buf.insert_text(" test ", 21);
    788         buf.insert_text("@elsat", 27);
    789         buf.select_full_mention(2, JB55());
    790         buf.insert_text(" test ", 33);
    791         buf.insert_text("@kernelkind", 39);
    792         buf.select_full_mention(3, KK());
    793         buf.insert_text(" test", 50);
    794 
    795         let mention1_bounds = 5..10;
    796         let mention2_bounds = 16..21;
    797         let mention3_bounds = 27..33;
    798         let mention4_bounds = 39..50;
    799 
    800         let text = "test @jb55 test @vrod test @elsat test @kernelkind test";
    801 
    802         assert_eq!(buf.as_str(), text);
    803         assert_eq!(buf.mentions.len(), 4);
    804 
    805         let mention1 = buf.mentions.get(&0).unwrap();
    806         assert_eq!(mention1.bounds(), mention1_bounds);
    807         assert_eq!(mention1.mention_type, MentionType::Finalized(JB55()));
    808         let mention2 = buf.mentions.get(&1).unwrap();
    809         assert_eq!(mention2.bounds(), mention2_bounds);
    810         assert_eq!(mention2.mention_type, MentionType::Finalized(JB55()));
    811         let mention3 = buf.mentions.get(&2).unwrap();
    812         assert_eq!(mention3.bounds(), mention3_bounds);
    813         assert_eq!(mention3.mention_type, MentionType::Finalized(JB55()));
    814         let mention4 = buf.mentions.get(&3).unwrap();
    815         assert_eq!(mention4.bounds(), mention4_bounds);
    816         assert_eq!(mention4.mention_type, MentionType::Finalized(KK()));
    817 
    818         let text = text.to_owned();
    819         MentionExample {
    820             text,
    821             mention1: Some(mention1.clone()),
    822             mention2: Some(mention2.clone()),
    823             mention3: Some(mention3.clone()),
    824             mention4: Some(mention4.clone()),
    825         }
    826     }
    827 
    828     impl PostBuffer {
    829         fn to_example(&self) -> MentionExample {
    830             let mention1 = self.mentions.get(&0).cloned();
    831             let mention2 = self.mentions.get(&1).cloned();
    832             let mention3 = self.mentions.get(&2).cloned();
    833             let mention4 = self.mentions.get(&3).cloned();
    834 
    835             MentionExample {
    836                 text: self.text_buffer.clone(),
    837                 mention1,
    838                 mention2,
    839                 mention3,
    840                 mention4,
    841             }
    842         }
    843     }
    844 
    845     impl MentionInfo {
    846         fn shifted(mut self, offset: usize) -> Self {
    847             self.end_index -= offset;
    848             self.start_index -= offset;
    849 
    850             self
    851         }
    852     }
    853 
    854     #[test]
    855     fn test_extract_hashtags() {
    856         let test_cases = vec![
    857             ("Hello #world", vec!["world"]),
    858             ("Multiple #tags #in #one post", vec!["tags", "in", "one"]),
    859             ("No hashtags here", vec![]),
    860             ("#tag1 with #tag2!", vec!["tag1", "tag2"]),
    861             ("Ignore # empty", vec![]),
    862             ("Testing emoji #🍌banana", vec!["🍌banana"]),
    863             ("Testing emoji #🍌", vec!["🍌"]),
    864             ("Duplicate #tag #tag #tag", vec!["tag"]),
    865             ("Mixed case #TaG #tag #TAG", vec!["tag"]),
    866             (
    867                 "#tag1, #tag2, #tag3 with commas",
    868                 vec!["tag1", "tag2", "tag3"],
    869             ),
    870             ("Separated by commas #tag1,#tag2", vec!["tag1", "tag2"]),
    871             ("Separated by periods #tag1.#tag2", vec!["tag1", "tag2"]),
    872             ("Separated by semicolons #tag1;#tag2", vec!["tag1", "tag2"]),
    873         ];
    874 
    875         for (input, expected) in test_cases {
    876             let result = NewPost::extract_hashtags(input);
    877             let expected: HashSet<String> = expected.into_iter().map(String::from).collect();
    878             assert_eq!(result, expected, "Failed for input: {}", input);
    879         }
    880     }
    881 
    882     #[test]
    883     fn test_insert_single_mention() {
    884         let mut buf = PostBuffer::default();
    885         buf.insert_text("test ", 0);
    886         buf.insert_text("@", 5);
    887         assert!(buf.get_mention(5).is_some());
    888         buf.insert_text("jb55", 6);
    889         assert_eq!(buf.as_str(), "test @jb55");
    890         assert_eq!(buf.mentions.len(), 1);
    891         assert_eq!(buf.mentions.get(&0).unwrap().bounds(), 5..10);
    892 
    893         buf.select_full_mention(0, JB55());
    894 
    895         assert_eq!(
    896             buf.mentions.get(&0).unwrap().mention_type,
    897             MentionType::Finalized(JB55())
    898         );
    899     }
    900 
    901     #[test]
    902     fn test_insert_mention_with_space() {
    903         let mut buf = PostBuffer::default();
    904         buf.insert_text("@", 0);
    905         buf.insert_text("jb", 1);
    906         buf.insert_text("55", 3);
    907         assert!(buf.get_mention(1).is_some());
    908         assert_eq!(buf.mentions.len(), 1);
    909         assert_eq!(buf.mentions.get(&0).unwrap().bounds(), 0..5);
    910         buf.insert_text(" test", 5);
    911         assert_eq!(buf.mentions.get(&0).unwrap().bounds(), 0..10);
    912         assert_eq!(buf.as_str(), "@jb55 test");
    913 
    914         buf.select_full_mention(0, JB55());
    915 
    916         assert_eq!(
    917             buf.mentions.get(&0).unwrap().mention_type,
    918             MentionType::Finalized(JB55())
    919         );
    920     }
    921 
    922     #[test]
    923     fn test_insert_mention_with_emojis() {
    924         let mut buf = PostBuffer::default();
    925         buf.insert_text("test ", 0);
    926         buf.insert_text("@test😀 🏴‍☠️ :D", 5);
    927         buf.select_full_mention(0, JB55());
    928         buf.insert_text(" test", 19);
    929 
    930         assert_eq!(buf.as_str(), "test @test😀 🏴‍☠️ :D test");
    931         let mention = buf.mentions.get(&0).unwrap();
    932         assert_eq!(
    933             *mention,
    934             MentionInfo {
    935                 start_index: 5,
    936                 end_index: 19,
    937                 mention_type: MentionType::Finalized(JB55())
    938             }
    939         );
    940     }
    941 
    942     #[test]
    943     fn test_insert_partial_to_full() {
    944         let mut buf = PostBuffer::default();
    945         buf.insert_text("@jb", 0);
    946         assert_eq!(buf.mentions.len(), 1);
    947         assert_eq!(buf.mentions.get(&0).unwrap().bounds(), 0..3);
    948         buf.select_mention_and_replace_name(0, "jb55", JB55());
    949         assert_eq!(buf.as_str(), "@jb55 ");
    950 
    951         buf.insert_text("test", 6);
    952         assert_eq!(buf.as_str(), "@jb55 test");
    953 
    954         assert_eq!(buf.mentions.len(), 1);
    955         let mention = buf.mentions.get(&0).unwrap();
    956         assert_eq!(mention.bounds(), 0..5);
    957         assert_eq!(mention.mention_type, MentionType::Finalized(JB55()));
    958     }
    959 
    960     #[test]
    961     fn test_insert_mention_after_text() {
    962         let mut buf = PostBuffer::default();
    963         buf.insert_text("test text here", 0);
    964         buf.insert_text("@jb55", 4);
    965 
    966         assert!(buf.mentions.is_empty());
    967     }
    968 
    969     #[test]
    970     fn test_insert_mention_with_space_after_text() {
    971         let mut buf = PostBuffer::default();
    972         buf.insert_text("test  text here", 0);
    973         buf.insert_text("@jb55", 5);
    974 
    975         assert!(buf.get_mention(5).is_some());
    976         assert_eq!(buf.mentions.len(), 1);
    977         assert_eq!(buf.mentions.get(&0).unwrap().bounds(), 5..10);
    978         assert_eq!("test @jb55 text here", buf.as_str());
    979 
    980         buf.select_full_mention(0, JB55());
    981 
    982         assert_eq!(
    983             buf.mentions.get(&0).unwrap().mention_type,
    984             MentionType::Finalized(JB55())
    985         );
    986     }
    987 
    988     #[test]
    989     fn test_insert_mention_then_text() {
    990         let mut buf = PostBuffer::default();
    991 
    992         buf.insert_text("@jb55", 0);
    993         buf.select_full_mention(0, JB55());
    994 
    995         buf.insert_text(" test", 5);
    996         assert_eq!(buf.as_str(), "@jb55 test");
    997         assert_eq!(buf.mentions.len(), 1);
    998         assert_eq!(buf.mentions.get(&0).unwrap().bounds(), 0..5);
    999         assert!(buf.get_mention(6).is_none());
   1000     }
   1001 
   1002     #[test]
   1003     fn test_insert_two_mentions() {
   1004         let mut buf = PostBuffer::default();
   1005 
   1006         buf.insert_text("@jb55", 0);
   1007         buf.select_full_mention(0, JB55());
   1008         buf.insert_text(" test ", 5);
   1009         buf.insert_text("@kernelkind", 11);
   1010         buf.select_full_mention(1, KK());
   1011         buf.insert_text(" test", 22);
   1012 
   1013         assert_eq!(buf.as_str(), "@jb55 test @kernelkind test");
   1014         assert_eq!(buf.mentions.len(), 2);
   1015         assert_eq!(buf.mentions.get(&0).unwrap().bounds(), 0..5);
   1016         assert_eq!(buf.mentions.get(&1).unwrap().bounds(), 11..22);
   1017     }
   1018 
   1019     #[test]
   1020     fn test_insert_into_mention() {
   1021         let mut buf = PostBuffer::default();
   1022 
   1023         buf.insert_text("@jb55", 0);
   1024         buf.select_full_mention(0, JB55());
   1025         buf.insert_text(" test", 5);
   1026 
   1027         assert_eq!(buf.mentions.len(), 1);
   1028         let mention = buf.mentions.get(&0).unwrap();
   1029         assert_eq!(mention.bounds(), 0..5);
   1030         assert_eq!(mention.mention_type, MentionType::Finalized(JB55()));
   1031 
   1032         buf.insert_text("oops", 2);
   1033         assert_eq!(buf.as_str(), "@joopsb55 test");
   1034         assert_eq!(buf.mentions.len(), 1);
   1035         let mention = buf.mentions.get(&0).unwrap();
   1036         assert_eq!(mention.bounds(), 0..9);
   1037         assert_eq!(mention.mention_type, MentionType::Pending);
   1038     }
   1039 
   1040     #[test]
   1041     fn test_insert_mention_inside_mention() {
   1042         let mut buf = PostBuffer::default();
   1043 
   1044         buf.insert_text("@jb55", 0);
   1045         buf.select_full_mention(0, JB55());
   1046         buf.insert_text(" test", 5);
   1047 
   1048         assert_eq!(buf.mentions.len(), 1);
   1049         let mention = buf.mentions.get(&0).unwrap();
   1050         assert_eq!(mention.bounds(), 0..5);
   1051         assert_eq!(mention.mention_type, MentionType::Finalized(JB55()));
   1052 
   1053         buf.insert_text(" ", 3);
   1054         buf.insert_text("@oops", 4);
   1055         assert_eq!(buf.as_str(), "@jb @oops55 test");
   1056         assert_eq!(buf.mentions.len(), 1);
   1057         assert_eq!(buf.mention_ends.len(), 1);
   1058         assert_eq!(buf.mention_starts.len(), 1);
   1059         let mention = buf.mentions.get(&1).unwrap();
   1060         assert_eq!(mention.bounds(), 4..9);
   1061         assert_eq!(mention.mention_type, MentionType::Pending);
   1062     }
   1063 
   1064     #[test]
   1065     fn test_delete_before_mention() {
   1066         let mut buf = PostBuffer::default();
   1067         let before = apply_mention_example(&mut buf);
   1068 
   1069         let range = 1..5;
   1070         let len = range.len();
   1071         buf.delete_char_range(range);
   1072 
   1073         assert_eq!(
   1074             MentionExample {
   1075                 text: "t@jb55 test @vrod test @elsat test @kernelkind test".to_owned(),
   1076                 mention1: Some(before.mention1.clone().unwrap().shifted(len)),
   1077                 mention2: Some(before.mention2.clone().unwrap().shifted(len)),
   1078                 mention3: Some(before.mention3.clone().unwrap().shifted(len)),
   1079                 mention4: Some(before.mention4.clone().unwrap().shifted(len)),
   1080             },
   1081             buf.to_example(),
   1082         );
   1083     }
   1084 
   1085     #[test]
   1086     fn test_delete_after_mention() {
   1087         let mut buf = PostBuffer::default();
   1088         let before = apply_mention_example(&mut buf);
   1089 
   1090         let range = 11..16;
   1091         let len = range.len();
   1092         buf.delete_char_range(range);
   1093 
   1094         assert_eq!(
   1095             MentionExample {
   1096                 text: "test @jb55 @vrod test @elsat test @kernelkind test".to_owned(),
   1097                 mention2: Some(before.mention2.clone().unwrap().shifted(len)),
   1098                 mention3: Some(before.mention3.clone().unwrap().shifted(len)),
   1099                 mention4: Some(before.mention4.clone().unwrap().shifted(len)),
   1100                 ..before.clone()
   1101             },
   1102             buf.to_example(),
   1103         );
   1104     }
   1105 
   1106     #[test]
   1107     fn test_delete_mention_partial_inner() {
   1108         let mut buf = PostBuffer::default();
   1109         let before = apply_mention_example(&mut buf);
   1110 
   1111         let range = 17..20;
   1112         let len = range.len();
   1113         buf.delete_char_range(range);
   1114 
   1115         assert_eq!(
   1116             MentionExample {
   1117                 text: "test @jb55 test @d test @elsat test @kernelkind test".to_owned(),
   1118                 mention2: Some(MentionInfo {
   1119                     start_index: 16,
   1120                     end_index: 18,
   1121                     mention_type: MentionType::Pending,
   1122                 }),
   1123                 mention3: Some(before.mention3.clone().unwrap().shifted(len)),
   1124                 mention4: Some(before.mention4.clone().unwrap().shifted(len)),
   1125                 ..before.clone()
   1126             },
   1127             buf.to_example(),
   1128         );
   1129     }
   1130 
   1131     #[test]
   1132     fn test_delete_mention_partial_outer() {
   1133         let mut buf = PostBuffer::default();
   1134         let before = apply_mention_example(&mut buf);
   1135 
   1136         let range = 17..27;
   1137         let len = range.len();
   1138         buf.delete_char_range(range);
   1139 
   1140         assert_eq!(
   1141             MentionExample {
   1142                 text: "test @jb55 test @@elsat test @kernelkind test".to_owned(),
   1143                 mention2: Some(MentionInfo {
   1144                     start_index: 16,
   1145                     end_index: 17,
   1146                     mention_type: MentionType::Pending
   1147                 }),
   1148                 mention3: Some(before.mention3.clone().unwrap().shifted(len)),
   1149                 mention4: Some(before.mention4.clone().unwrap().shifted(len)),
   1150                 ..before.clone()
   1151             },
   1152             buf.to_example(),
   1153         );
   1154     }
   1155 
   1156     #[test]
   1157     fn test_delete_mention_partial_and_full() {
   1158         let mut buf = PostBuffer::default();
   1159         let before = apply_mention_example(&mut buf);
   1160 
   1161         buf.delete_char_range(17..28);
   1162 
   1163         assert_eq!(
   1164             MentionExample {
   1165                 text: "test @jb55 test @elsat test @kernelkind test".to_owned(),
   1166                 mention2: Some(MentionInfo {
   1167                     end_index: 17,
   1168                     mention_type: MentionType::Pending,
   1169                     ..before.mention2.clone().unwrap()
   1170                 }),
   1171                 mention3: None,
   1172                 mention4: Some(MentionInfo {
   1173                     start_index: 28,
   1174                     end_index: 39,
   1175                     ..before.mention4.clone().unwrap()
   1176                 }),
   1177                 ..before.clone()
   1178             },
   1179             buf.to_example()
   1180         )
   1181     }
   1182 
   1183     #[test]
   1184     fn test_delete_mention_full_one() {
   1185         let mut buf = PostBuffer::default();
   1186         let before = apply_mention_example(&mut buf);
   1187 
   1188         let range = 10..26;
   1189         let len = range.len();
   1190         buf.delete_char_range(range);
   1191 
   1192         assert_eq!(
   1193             MentionExample {
   1194                 text: "test @jb55 @elsat test @kernelkind test".to_owned(),
   1195                 mention2: None,
   1196                 mention3: Some(before.mention3.clone().unwrap().shifted(len)),
   1197                 mention4: Some(before.mention4.clone().unwrap().shifted(len)),
   1198                 ..before.clone()
   1199             },
   1200             buf.to_example()
   1201         );
   1202     }
   1203 
   1204     #[test]
   1205     fn test_delete_mention_full_two() {
   1206         let mut buf = PostBuffer::default();
   1207         let before = apply_mention_example(&mut buf);
   1208 
   1209         buf.delete_char_range(11..28);
   1210 
   1211         assert_eq!(
   1212             MentionExample {
   1213                 text: "test @jb55 elsat test @kernelkind test".to_owned(),
   1214                 mention2: None,
   1215                 mention3: None,
   1216                 mention4: Some(MentionInfo {
   1217                     start_index: 22,
   1218                     end_index: 33,
   1219                     ..before.mention4.clone().unwrap()
   1220                 }),
   1221                 ..before.clone()
   1222             },
   1223             buf.to_example()
   1224         )
   1225     }
   1226 
   1227     #[test]
   1228     fn test_two_then_one_between() {
   1229         let mut buf = PostBuffer::default();
   1230 
   1231         buf.insert_text("@jb", 0);
   1232         buf.select_mention_and_replace_name(0, "jb55", JB55());
   1233         buf.insert_text("test ", 6);
   1234         assert_eq!(buf.as_str(), "@jb55 test ");
   1235         buf.insert_text("@kernel", 11);
   1236         buf.select_mention_and_replace_name(1, "KernelKind", KK());
   1237         assert_eq!(buf.as_str(), "@jb55 test @KernelKind ");
   1238 
   1239         buf.insert_text("test", 23);
   1240         assert_eq!(buf.as_str(), "@jb55 test @KernelKind test");
   1241 
   1242         assert_eq!(buf.mentions.len(), 2);
   1243 
   1244         buf.insert_text("@els", 6);
   1245         assert_eq!(buf.as_str(), "@jb55 @elstest @KernelKind test");
   1246 
   1247         assert_eq!(buf.mentions.len(), 3);
   1248         assert_eq!(buf.mentions.get(&2).unwrap().bounds(), 6..10);
   1249         buf.select_mention_and_replace_name(2, "elsat", JB55());
   1250         assert_eq!(buf.as_str(), "@jb55 @elsat test @KernelKind test");
   1251 
   1252         let jb_mention = buf.mentions.get(&0).unwrap();
   1253         let kk_mention = buf.mentions.get(&1).unwrap();
   1254         let el_mention = buf.mentions.get(&2).unwrap();
   1255         assert_eq!(jb_mention.bounds(), 0..5);
   1256         assert_eq!(jb_mention.mention_type, MentionType::Finalized(JB55()));
   1257         assert_eq!(kk_mention.bounds(), 18..29);
   1258         assert_eq!(kk_mention.mention_type, MentionType::Finalized(KK()));
   1259         assert_eq!(el_mention.bounds(), 6..12);
   1260         assert_eq!(el_mention.mention_type, MentionType::Finalized(JB55()));
   1261     }
   1262 
   1263     #[test]
   1264     fn note_single_mention() {
   1265         let mut buf = PostBuffer::default();
   1266         buf.insert_text("@jb55", 0);
   1267         buf.select_full_mention(0, JB55());
   1268 
   1269         let out = buf.output();
   1270         let kp = FullKeypair::generate();
   1271         let post = NewPost::new(out.text, kp.clone(), Vec::new(), out.mentions);
   1272         let note = post.to_note(&kp.pubkey);
   1273 
   1274         let mut tags_iter = note.tags().iter();
   1275         tags_iter.next(); //ignore the first one, the client tag
   1276         let tag = tags_iter.next().unwrap();
   1277         assert_eq!(tag.count(), 2);
   1278         assert_eq!(tag.get(0).unwrap().str().unwrap(), "p");
   1279         assert_eq!(tag.get(1).unwrap().id().unwrap(), JB55().bytes());
   1280         assert!(tags_iter.next().is_none());
   1281         assert_eq!(
   1282             note.content(),
   1283             "nostr:npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s"
   1284         );
   1285     }
   1286 
   1287     #[test]
   1288     fn note_two_mentions() {
   1289         let mut buf = PostBuffer::default();
   1290 
   1291         buf.insert_text("@jb55", 0);
   1292         buf.select_full_mention(0, JB55());
   1293         buf.insert_text(" test ", 5);
   1294         buf.insert_text("@KernelKind", 11);
   1295         buf.select_full_mention(1, KK());
   1296         buf.insert_text(" test", 22);
   1297         assert_eq!(buf.as_str(), "@jb55 test @KernelKind test");
   1298 
   1299         let out = buf.output();
   1300         let kp = FullKeypair::generate();
   1301         let post = NewPost::new(out.text, kp.clone(), Vec::new(), out.mentions);
   1302         let note = post.to_note(&kp.pubkey);
   1303 
   1304         let mut tags_iter = note.tags().iter();
   1305         tags_iter.next(); //ignore the first one, the client tag
   1306         let jb_tag = tags_iter.next().unwrap();
   1307         assert_eq!(jb_tag.count(), 2);
   1308         assert_eq!(jb_tag.get(0).unwrap().str().unwrap(), "p");
   1309         assert_eq!(jb_tag.get(1).unwrap().id().unwrap(), JB55().bytes());
   1310 
   1311         let kk_tag = tags_iter.next().unwrap();
   1312         assert_eq!(kk_tag.count(), 2);
   1313         assert_eq!(kk_tag.get(0).unwrap().str().unwrap(), "p");
   1314         assert_eq!(kk_tag.get(1).unwrap().id().unwrap(), KK().bytes());
   1315 
   1316         assert!(tags_iter.next().is_none());
   1317 
   1318         assert_eq!(note.content(), "nostr:npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s test nostr:npub1fgz3pungsr2quse0fpjuk4c5m8fuyqx2d6a3ddqc4ek92h6hf9ns0mjeck test");
   1319     }
   1320 
   1321     #[test]
   1322     fn note_one_pending() {
   1323         let mut buf = PostBuffer::default();
   1324 
   1325         buf.insert_text("test ", 0);
   1326         buf.insert_text("@jb55 test", 5);
   1327 
   1328         let out = buf.output();
   1329         let kp = FullKeypair::generate();
   1330         let post = NewPost::new(out.text, kp.clone(), Vec::new(), out.mentions);
   1331         let note = post.to_note(&kp.pubkey);
   1332 
   1333         let mut tags_iter = note.tags().iter();
   1334         tags_iter.next(); //ignore the first one, the client tag
   1335         assert!(tags_iter.next().is_none());
   1336         assert_eq!(note.content(), "test @jb55 test");
   1337     }
   1338 }