notedeck

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

post.rs (43995B)


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