@@ -74,7 +74,9 @@ fn mention_tags(mentions: &[&str]) -> Result<Vec<Tag>, String> {
7474 check_pubkey ( hex) ?;
7575 let lower = hex. to_ascii_lowercase ( ) ;
7676 if seen. insert ( lower. clone ( ) ) {
77+ // Parallel p (fan-out) + mention (explicit intent) for #2296 contract.
7778 tags. push ( tag ( vec ! [ "p" , & lower] ) ?) ;
79+ tags. push ( tag ( vec ! [ "mention" , & lower] ) ?) ;
7880 }
7981 }
8082 Ok ( tags)
@@ -356,6 +358,19 @@ pub struct MessageEditTags<'a> {
356358
357359/// Kind 40003 — edit a message with full content, media, emoji, mentions,
358360/// and optional monotonic link-preview suppression.
361+ ///
362+ /// Carries the full new content AND a fresh imeta tag set; the receiver
363+ /// overlays the imeta tags onto the original event so the rendered message
364+ /// reflects exactly the edited state. NIP-30 custom-emoji tags ride along the
365+ /// same way so an edited body's `:shortcode:`s stay resolvable (the send path
366+ /// attaches these too).
367+ ///
368+ /// `mentions` carries the pubkeys of mentions that are *newly added* by this
369+ /// edit (the caller diffs the edited body against the original). Only those get
370+ /// parallel `p` + `mention` tags so the newly-mentioned party is notified/woken,
371+ /// while a typo-fix edit that leaves the mention set unchanged emits neither
372+ /// and never re-wakes anyone. Mirrors the send path's `mention_tags` (dedup +
373+ /// lowercase); the receiver overlays these onto the original event's audience.
359374pub fn build_message_edit (
360375 channel_id : Uuid ,
361376 target_event_id : EventId ,
@@ -850,6 +865,14 @@ mod tests {
850865 assert_eq ! ( event. pubkey. to_hex( ) , TARGET_HEX ) ;
851866 }
852867
868+ // ── build_message_edit p+mention emission (lane 8ace8eed) ──────────────
869+ //
870+ // The composer diffs the edited body's mentions against the original and
871+ // hands `build_message_edit` only the *newly added* pubkeys. These tests
872+ // pins the builder's contract: emit parallel `p` + `mention` per added
873+ // mention (deduped, lowercased), and none when the added set is empty
874+ // (typo-fix edit) — so an unchanged mention set re-wakes nobody.
875+
853876 const CH_ID : & str = "11111111-1111-4111-8111-111111111111" ;
854877 const ALICE_HEX : & str = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" ;
855878 const BOB_HEX : & str = "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" ;
@@ -892,19 +915,21 @@ mod tests {
892915 let tags = edit_tags ( & [ ALICE_HEX ] ) ;
893916 assert_eq ! ( tags[ 0 ] [ 0 ] , "h" ) ;
894917 assert_eq ! ( tags[ 1 ] [ 0 ] , "e" ) ;
918+ // `p` + parallel `mention` ride right after the `e` tag (insertion order).
895919 assert_eq ! ( tags[ 2 ] , vec![ "p" . to_string( ) , ALICE_HEX . to_string( ) ] ) ;
920+ assert_eq ! ( tags[ 3 ] , vec![ "mention" . to_string( ) , ALICE_HEX . to_string( ) ] ) ;
896921 }
897922
898923 #[ test]
899924 fn edit_with_no_added_mentions_emits_no_p_tag ( ) {
900925 // Typo-fix edit: mention set unchanged, so the composer passes `&[]`.
901- // The edit event must carry no `p` tag and re-wake nobody.
926+ // The edit event must carry no `p`/`mention` tags and re-wake nobody.
902927 let tags = edit_tags ( & [ ] ) ;
903928 assert ! (
904- !tags
905- . iter ( )
906- . any ( |t| t . first ( ) . map ( String :: as_str ) == Some ( "p" ) ) ,
907- "unchanged-mention edit must not emit any `p` tag , got {tags:?}"
929+ !tags. iter ( ) . any ( |t| {
930+ matches! ( t . first ( ) . map ( String :: as_str ) , Some ( "p" | "mention" ) )
931+ } ) ,
932+ "unchanged-mention edit must not emit p/mention tags , got {tags:?}"
908933 ) ;
909934 }
910935
@@ -960,5 +985,18 @@ mod tests {
960985 ) ;
961986 assert_eq ! ( p_tags[ 0 ] , & vec![ "p" . to_string( ) , ALICE_HEX . to_string( ) ] ) ;
962987 assert_eq ! ( p_tags[ 1 ] , & vec![ "p" . to_string( ) , BOB_HEX . to_string( ) ] ) ;
988+ let mention_tags: Vec < & Vec < String > > = tags
989+ . iter ( )
990+ . filter ( |t| t. first ( ) . map ( String :: as_str) == Some ( "mention" ) )
991+ . collect ( ) ;
992+ assert_eq ! ( mention_tags. len( ) , 2 ) ;
993+ assert_eq ! (
994+ mention_tags[ 0 ] ,
995+ & vec![ "mention" . to_string( ) , ALICE_HEX . to_string( ) ]
996+ ) ;
997+ assert_eq ! (
998+ mention_tags[ 1 ] ,
999+ & vec![ "mention" . to_string( ) , BOB_HEX . to_string( ) ]
1000+ ) ;
9631001 }
9641002}
0 commit comments