notedeck

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

post.rs (45559B)


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