chore: convert NIP-21 references to njump link

This commit is contained in:
2026-10-12 11:02:15 +07:00
parent 291471075b
commit 7c42466b0a
4 changed files with 129 additions and 31 deletions
+113 -1
View File
@@ -8,6 +8,9 @@ use state::FileAttachment;
pub(crate) const KIND_FILE_MESSAGE: Kind = Kind::Custom(15);
const SCHEME_WITH_COLON: &str = "nostr:";
const NEVENT_PREFIX: &str = "nevent1";
/// Rendered message.
#[derive(Debug, Clone)]
pub struct Message {
@@ -148,7 +151,33 @@ impl Message {
return format!("[File] {}", file.display_name()).into();
}
self.content.clone().into()
// Media-only messages carry their URLs outside `.content`
if self.content.is_empty() && !self.media.is_empty() {
return self
.media
.iter()
.map(|url| url.to_string())
.collect::<Vec<_>>()
.join(" ")
.into();
}
let references = nevent_refs(&self.content);
if references.is_empty() {
return self.content.clone().into();
}
let mut preview = String::with_capacity(self.content.len());
let mut last = 0;
for reference in references {
preview.push_str(&self.content[last..reference.range.start]);
preview.push_str(&reference.label);
last = reference.range.end;
}
preview.push_str(&self.content[last..]);
preview.into()
}
/// Image URL of a `:shortcode:` reaction, if the shortcode is declared
@@ -219,6 +248,61 @@ fn extract_mentions(content: &str) -> Vec<Mention> {
.collect()
}
/// A `nevent` reference found in a content string.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NeventRef {
/// Range of the reference, including the optional `nostr:` scheme
pub range: Range<usize>,
/// Compact display label, like `https://njump.me/nevent1...d9sp`
pub label: String,
}
/// Extracts valid `nevent1...` and `nostr:nevent1...` references from content.
pub fn nevent_refs(content: &str) -> Vec<NeventRef> {
let mut references = Vec::new();
for (match_start, _) in content.match_indices(NEVENT_PREFIX) {
// Only lowercase references are matched, matching NIP-19 encodings in practice
let start = if content[..match_start].ends_with(SCHEME_WITH_COLON) {
match_start - SCHEME_WITH_COLON.len()
} else {
match_start
};
// Skip matches glued to word characters, like the `nevent1` in `xnevent1...`
if content[..start].ends_with(char::is_alphanumeric) {
continue;
}
let end = match_start
+ content[match_start..]
.bytes()
.take_while(u8::is_ascii_alphanumeric)
.count();
// The bech32 token excludes the optional `nostr:` scheme
let bech32 = &content[match_start..end];
if Nip19Event::from_bech32(bech32).is_err() {
continue;
}
references.push(NeventRef {
range: start..end,
label: nevent_label(bech32),
});
}
references
}
/// Compact display of a nevent link, like `https://njump.me/nevent1...d9sp`.
fn nevent_label(bech32: &str) -> String {
match bech32.len() {
0..=4 => format!("https://njump.me/{bech32}"),
_ => format!("https://njump.me/nevent1...{}", &bech32[bech32.len() - 4..]),
}
}
/// Extracts all reply (ids) from the event tags.
fn extract_reply_ids(inner: &Tags) -> Vec<EventId> {
let mut replies_to = vec![];
@@ -283,4 +367,32 @@ mod tests {
assert_eq!(Message::reaction_image("😂", &emojis), None);
assert_eq!(Message::reaction_image(":unknown:", &emojis), None);
}
fn valid_nevent() -> String {
Nip19Event::new(EventId::from_byte_array([7; 32]))
.to_bech32()
.expect("valid nevent")
}
#[test]
fn image_only_messages_preview_their_url() {
let url = "https://example.com/picture.png";
let message = Message::from(&rumor(Kind::PrivateDirectMessage, url, Tags::new()));
assert!(message.content.is_empty());
assert_eq!(message.preview().to_string(), url);
}
#[test]
fn preview_compacts_nevent_references() {
let nevent = valid_nevent();
let message = Message::from(&rumor(
Kind::PrivateDirectMessage,
&format!("see nostr:{nevent}"),
Tags::new(),
));
let label = format!("https://njump.me/nevent1...{}", &nevent[nevent.len() - 4..]);
assert_eq!(message.preview().to_string(), format!("see {label}"));
}
}