feat: add support for custom emojis (NIP-30) (#63)

Reviewed-on: #63
This commit was merged in pull request #63.
This commit is contained in:
2026-10-11 13:22:53 +00:00
parent b4ce9368e2
commit 291471075b
16 changed files with 493 additions and 63 deletions
+28 -7
View File
@@ -1,8 +1,8 @@
use std::ops::Range;
use gpui::{
AnyElement, App, ElementId, HighlightStyle, IntoElement, SharedString, StyleRefinement,
Styled as _, Window,
AnyElement, App, ElementId, HighlightStyle, IntoElement, SharedString, SharedUri,
StyleRefinement, Styled as _, Window,
};
use gpui_base::{TextView, TextViewStyle};
use theme::ActiveTheme as _;
@@ -12,6 +12,8 @@ use theme::ActiveTheme as _;
pub struct InlineReplacement {
pub range: Range<usize>,
pub text: SharedString,
/// When set, the span renders as this image (NIP-30 custom emoji)
pub image: Option<SharedUri>,
}
impl InlineReplacement {
@@ -19,6 +21,19 @@ impl InlineReplacement {
Self {
range,
text: text.into(),
image: None,
}
}
pub fn image(
range: Range<usize>,
alt: impl Into<SharedString>,
url: impl Into<SharedUri>,
) -> Self {
Self {
range,
text: alt.into(),
image: Some(url.into()),
}
}
}
@@ -46,11 +61,17 @@ impl RenderedText {
}
text.push_str(&content[last..range.start]);
text.push_str(&format!(
"[{}](<{}>)",
escape_link_text(&replacement.text),
&content[range.start..range.end]
));
match &replacement.image {
Some(url) => text.push_str(&format!(
"![{}](<{url}>)",
escape_link_text(&replacement.text)
)),
None => text.push_str(&format!(
"[{}](<{}>)",
escape_link_text(&replacement.text),
&content[range.start..range.end]
)),
}
last = range.end;
}
text.push_str(&content[last..]);
+19 -6
View File
@@ -370,26 +370,36 @@ impl RenderOnce for MessageMedia {
/// Reactions of one emoji with the count of authors and whether the current user reacted.
pub struct ReactionGroup {
pub emoji: SharedString,
/// Image of a custom emoji (NIP-30) reaction
pub image: Option<SharedUri>,
pub count: usize,
pub mine: bool,
}
/// Group (emoji, author) pairs by emoji, sorted by emoji, marking groups containing `me`.
/// Group (emoji, image, author) triples by emoji, sorted by emoji.
pub fn group_reactions<A: PartialEq>(
reactions: impl IntoIterator<Item = (SharedString, A)>,
reactions: impl IntoIterator<Item = (SharedString, Option<SharedUri>, A)>,
me: Option<A>,
) -> Vec<ReactionGroup> {
let mut groups: BTreeMap<SharedString, (usize, bool)> = BTreeMap::new();
let mut groups: BTreeMap<SharedString, (usize, bool, Option<SharedUri>)> = BTreeMap::new();
for (emoji, author) in reactions {
for (emoji, image, author) in reactions {
let group = groups.entry(emoji).or_default();
group.0 += 1;
group.1 |= me.as_ref() == Some(&author);
if group.2.is_none() {
group.2 = image;
}
}
groups
.into_iter()
.map(|(emoji, (count, mine))| ReactionGroup { emoji, count, mine })
.map(|(emoji, (count, mine, image))| ReactionGroup {
emoji,
image,
count,
mine,
})
.collect()
}
@@ -438,7 +448,10 @@ impl RenderOnce for ReactionBar {
.border_color(cx.theme().element_active)
})
.when(!group.mine, |this| this.border_color(cx.theme().border))
.child(group.emoji)
.when(group.image.is_none(), |this| this.child(group.emoji))
.when_some(group.image, |this, image| {
this.child(img(image).size(px(16.)).flex_shrink_0())
})
.child(SharedString::from(group.count.to_string()))
}))
}