{
+ let Some(message) = self.messages.iter().nth(ix) else {
+ return div().id(ix);
+ };
+
+ let proxy = AppSettings::get_proxy_user_avatars(cx);
+ let hide_avatar = AppSettings::get_hide_user_avatars(cx);
+
+ let id = message.id;
+ let author = self.profile(&message.author, cx);
+
+ let replies = message.replies_to.as_slice();
+ let has_replies = !replies.is_empty();
+
+ // Check if message is sent failed
+ let is_sent_failed = self.is_sent_failed(&id);
+
+ // Check if message is sent successfully
+ let is_sent_success = self.is_sent_success(&id);
+
+ // Get or insert rendered text
+ let rendered_text = self
+ .rendered_texts_by_id
+ .entry(id)
+ .or_insert_with(|| RenderedText::new(&message.content, cx))
+ .element(ix.into(), window, cx);
+
+ div()
+ .id(ix)
+ .group("")
+ .relative()
+ .w_full()
+ .py_1()
+ .px_3()
+ .child(
+ div()
+ .flex()
+ .gap_3()
+ .when(!hide_avatar, |this| {
+ this.child(Avatar::new(author.avatar_url(proxy)).size(rems(2.)))
+ })
+ .child(
+ v_flex()
+ .flex_1()
+ .w_full()
+ .flex_initial()
+ .overflow_hidden()
+ .child(
+ div()
+ .flex()
+ .items_center()
+ .gap_2()
+ .text_sm()
+ .text_color(cx.theme().text_placeholder)
+ .child(
+ div()
+ .font_semibold()
+ .text_color(cx.theme().text)
+ .child(author.display_name()),
+ )
+ .child(div().child(message.ago()))
+ .when_some(is_sent_success, |this, status| {
+ this.when(status, |this| {
+ this.child(self.render_message_sent(&id, cx))
+ })
+ }),
+ )
+ .when(has_replies, |this| {
+ this.children(self.render_message_replies(replies, cx))
+ })
+ .child(rendered_text)
+ .when(is_sent_failed, |this| {
+ this.child(self.render_message_reports(&id, cx))
+ }),
+ ),
+ )
+ .child(self.render_border(cx))
+ .child(self.render_actions(&id, cx))
+ .on_mouse_down(
+ MouseButton::Middle,
+ cx.listener(move |this, _event, _window, cx| {
+ this.copy_message(&id, cx);
+ }),
+ )
+ .on_double_click(cx.listener({
+ move |this, _event, _window, cx| {
+ this.reply_to(&id, cx);
+ }
+ }))
+ .hover(|this| this.bg(cx.theme().surface_background))
+ }
+
+ fn render_message_replies(
+ &self,
+ replies: &[EventId],
+ cx: &Context
,
+ ) -> impl IntoIterator- {
+ let mut items = Vec::with_capacity(replies.len());
+
+ for (ix, id) in replies.iter().enumerate() {
+ let Some(message) = self.message(id) else {
+ continue;
+ };
+ let author = self.profile(&message.author, cx);
+
+ items.push(
+ div()
+ .id(ix)
+ .w_full()
+ .px_2()
+ .border_l_2()
+ .border_color(cx.theme().element_selected)
+ .text_sm()
+ .child(
+ div()
+ .text_color(cx.theme().text_accent)
+ .child(author.display_name()),
+ )
+ .child(
+ div()
+ .w_full()
+ .text_ellipsis()
+ .line_clamp(1)
+ .child(message.content.clone()),
+ )
+ .hover(|this| this.bg(cx.theme().elevated_surface_background))
+ .on_click({
+ let id = *id;
+ cx.listener(move |this, _event, _window, _cx| {
+ this.scroll_to(id);
+ })
+ }),
+ );
+ }
+
+ items
+ }
+
+ fn render_message_sent(&self, id: &EventId, _cx: &Context) -> impl IntoElement {
+ div().id("").child(shared_t!("chat.sent")).when_some(
+ self.sent_reports(id).cloned(),
+ |this, reports| {
+ this.on_click(move |_e, window, cx| {
+ let reports = reports.clone();
+
+ window.open_modal(cx, move |this, _window, cx| {
+ this.title(shared_t!("chat.reports")).child(
+ v_flex().pb_4().gap_4().children({
+ let mut items = Vec::with_capacity(reports.len());
+
+ for report in reports.iter() {
+ items.push(Self::render_report(report, cx))
+ }
+
+ items
+ }),
+ )
+ });
+ })
+ },
+ )
+ }
+
+ fn render_message_reports(&self, id: &EventId, cx: &Context) -> impl IntoElement {
+ h_flex()
+ .id("")
+ .gap_1()
+ .text_color(cx.theme().danger_foreground)
+ .text_xs()
+ .italic()
+ .child(Icon::new(IconName::Info).small())
+ .child(shared_t!("chat.sent_failed"))
+ .when_some(self.sent_reports(id).cloned(), |this, reports| {
+ this.on_click(move |_e, window, cx| {
+ let reports = reports.clone();
+
+ window.open_modal(cx, move |this, _window, cx| {
+ this.title(shared_t!("chat.reports")).child(
+ v_flex().pb_4().gap_4().children({
+ let mut items = Vec::with_capacity(reports.len());
+
+ for report in reports.iter() {
+ items.push(Self::render_report(report, cx))
+ }
+
+ items
+ }),
+ )
+ });
+ })
+ })
+ }
+
+ fn render_report(report: &SendReport, cx: &App) -> impl IntoElement {
+ let registry = Registry::read_global(cx);
+ let profile = registry.get_person(&report.receiver, cx);
+ let name = profile.display_name();
+ let avatar = profile.avatar_url(true);
+
+ v_flex()
+ .gap_2()
+ .child(
+ h_flex()
+ .gap_2()
+ .text_sm()
+ .child(shared_t!("chat.sent_to"))
+ .child(
+ h_flex()
+ .gap_1()
+ .font_semibold()
+ .child(Avatar::new(avatar).size(rems(1.25)))
+ .child(name.clone()),
+ ),
+ )
+ .when(report.nip17_relays_not_found, |this| {
+ this.child(
+ h_flex()
+ .flex_wrap()
+ .justify_center()
+ .p_2()
+ .h_20()
+ .w_full()
+ .text_sm()
+ .rounded(cx.theme().radius)
+ .bg(cx.theme().danger_background)
+ .text_color(cx.theme().danger_foreground)
+ .child(
+ div()
+ .flex_1()
+ .w_full()
+ .text_center()
+ .child(shared_t!("chat.nip17_not_found", u = name)),
+ ),
+ )
+ })
+ .when_some(report.local_error.clone(), |this, error| {
+ this.child(
+ h_flex()
+ .flex_wrap()
+ .justify_center()
+ .p_2()
+ .h_20()
+ .w_full()
+ .text_sm()
+ .rounded(cx.theme().radius)
+ .bg(cx.theme().danger_background)
+ .text_color(cx.theme().danger_foreground)
+ .child(div().flex_1().w_full().text_center().child(error)),
+ )
+ })
+ .when_some(report.output.clone(), |this, output| {
+ this.child(
+ v_flex()
+ .gap_2()
+ .text_xs()
+ .children({
+ let mut items = Vec::with_capacity(output.failed.len());
+
+ for (url, msg) in output.failed.into_iter() {
+ items.push(
+ h_flex()
+ .gap_1()
+ .justify_between()
+ .text_sm()
+ .child(
+ div()
+ .flex_1()
+ .py_0p5()
+ .px_2()
+ .bg(cx.theme().elevated_surface_background)
+ .rounded_sm()
+ .child(url.to_string()),
+ )
+ .child(
+ div()
+ .flex_1()
+ .py_0p5()
+ .px_2()
+ .bg(cx.theme().danger_background)
+ .text_color(cx.theme().danger_foreground)
+ .rounded_sm()
+ .child(msg.to_string()),
+ ),
+ )
+ }
+
+ items
+ })
+ .children({
+ let mut items = Vec::with_capacity(output.success.len());
+
+ for url in output.success.into_iter() {
+ items.push(
+ h_flex()
+ .gap_1()
+ .justify_between()
+ .text_sm()
+ .child(
+ div()
+ .flex_1()
+ .py_0p5()
+ .px_2()
+ .bg(cx.theme().elevated_surface_background)
+ .rounded_sm()
+ .child(url.to_string()),
+ )
+ .child(
+ div()
+ .flex_1()
+ .py_0p5()
+ .px_2()
+ .bg(cx.theme().secondary_background)
+ .text_color(cx.theme().secondary_foreground)
+ .rounded_sm()
+ .child(shared_t!("chat.sent_success")),
+ ),
+ )
+ }
+
+ items
+ }),
+ )
+ })
+ }
+
+ fn render_border(&self, cx: &Context) -> impl IntoElement {
+ div()
+ .group_hover("", |this| this.bg(cx.theme().element_active))
+ .absolute()
+ .left_0()
+ .top_0()
+ .w(px(2.))
+ .h_full()
+ .bg(cx.theme().border_transparent)
+ }
+
+ fn render_actions(&self, id: &EventId, cx: &Context) -> impl IntoElement {
+ let groups = vec![
+ Button::new("reply")
+ .icon(IconName::Reply)
+ .tooltip(t!("chat.reply_button"))
+ .small()
+ .ghost()
+ .on_click({
+ let id = id.to_owned();
+ cx.listener(move |this, _event, _window, cx| {
+ this.reply_to(&id, cx);
+ })
+ }),
+ Button::new("copy")
+ .icon(IconName::Copy)
+ .tooltip(t!("chat.copy_message_button"))
+ .small()
+ .ghost()
+ .on_click({
+ let id = id.to_owned();
+ cx.listener(move |this, _event, _window, cx| {
+ this.copy_message(&id, cx);
+ })
+ }),
+ ];
+
+ h_flex()
+ .p_0p5()
+ .gap_1()
+ .invisible()
+ .absolute()
+ .right_4()
+ .top_neg_2()
+ .shadow_sm()
+ .rounded_md()
+ .border_1()
+ .border_color(cx.theme().border)
+ .bg(cx.theme().background)
+ .children(groups)
+ .group_hover("", |this| this.visible())
+ }
+
+ fn render_attachment(&self, url: &Url, cx: &Context) -> impl IntoElement {
let url = url.clone();
let path: SharedString = url.to_string().into();
@@ -518,269 +931,87 @@ impl Chat {
}))
}
- fn render_reply_to(&mut self, message: &Message, cx: &Context) -> impl IntoElement {
- let registry = Registry::read_global(cx);
- let profile = registry.get_person(&message.author, cx);
+ fn render_attachment_list(
+ &self,
+ _window: &Window,
+ cx: &Context,
+ ) -> impl IntoIterator
- {
+ let mut items = vec![];
- div()
- .w_full()
- .pl_2()
- .border_l_2()
- .border_color(cx.theme().element_active)
- .child(
- div()
- .flex()
- .items_center()
- .justify_between()
- .child(
- div()
- .flex()
- .items_baseline()
- .gap_1()
- .text_xs()
- .text_color(cx.theme().text_muted)
- .child(SharedString::new(t!("chat.replying_to_label")))
- .child(
- div()
- .text_color(cx.theme().text_accent)
- .child(profile.display_name()),
- ),
- )
- .child(
- Button::new("remove-reply")
- .icon(IconName::Close)
- .xsmall()
- .ghost()
- .on_click({
- let id = message.id;
- cx.listener(move |this, _, _, cx| {
- this.remove_reply(id, cx);
- })
- }),
- ),
- )
- .child(
- div()
- .w_full()
- .text_sm()
- .text_ellipsis()
- .line_clamp(1)
- .child(message.content.clone()),
- )
+ for url in self.attachments.read(cx).iter() {
+ items.push(self.render_attachment(url, cx));
+ }
+
+ items
}
- fn render_message(
- &mut self,
- ix: usize,
- window: &mut Window,
- cx: &mut Context,
- ) -> impl IntoElement {
- let Some(message) = self.messages.read(cx).iter().nth(ix) else {
- return div().id(ix);
- };
+ fn render_reply(&self, id: &EventId, cx: &Context) -> impl IntoElement {
+ if let Some(text) = self.message(id) {
+ let registry = Registry::read_global(cx);
+ let profile = registry.get_person(&text.author, cx);
- let proxy = AppSettings::get_proxy_user_avatars(cx);
- let hide_avatar = AppSettings::get_hide_user_avatars(cx);
- let registry = Registry::read_global(cx);
- let author = registry.get_person(&message.author, cx);
-
- let texts = self
- .text_data
- .entry(message.id)
- .or_insert_with(|| RichText::new(&message.content, cx));
-
- div()
- .id(ix)
- .group("")
- .relative()
- .w_full()
- .py_1()
- .px_3()
- .child(
- div()
- .flex()
- .gap_3()
- .when(!hide_avatar, |this| {
- this.child(Avatar::new(author.avatar_url(proxy)).size(rems(2.)))
- })
- .child(
- div()
- .flex_1()
- .flex()
- .flex_col()
- .flex_initial()
- .overflow_hidden()
- .child(
- div()
- .flex()
- .items_baseline()
- .gap_2()
- .text_sm()
- .child(
- div()
- .font_semibold()
- .text_color(cx.theme().text)
- .child(author.display_name()),
- )
- .child(
- div()
- .text_color(cx.theme().text_placeholder)
- .child(message.ago()),
- ),
- )
- .when_some(message.replies_to.as_ref(), |this, replies| {
- this.w_full().children({
- let mut items = Vec::with_capacity(replies.len());
- let messages = self.messages.read(cx);
-
- for (ix, id) in replies.iter().cloned().enumerate() {
- let Some(message) = messages.iter().find(|m| m.id == id)
- else {
- continue;
- };
-
- items.push(
- div()
- .id(ix)
- .w_full()
- .px_2()
- .border_l_2()
- .border_color(cx.theme().element_selected)
- .text_sm()
- .child(
- div()
- .text_color(cx.theme().text_accent)
- .child(author.display_name()),
- )
- .child(
- div()
- .w_full()
- .text_ellipsis()
- .line_clamp(1)
- .child(message.content.clone()),
- )
- .hover(|this| {
- this.bg(cx.theme().elevated_surface_background)
- })
- .on_click(cx.listener(move |this, _, _, cx| {
- this.scroll_to(id, cx)
- })),
- );
- }
-
- items
- })
- })
- .child(texts.element(ix.into(), window, cx))
- .when_some(message.errors.as_ref(), |this, errors| {
- this.child(self.render_message_errors(errors, cx))
- }),
- ),
- )
- .child(self.render_border(cx))
- .child(self.render_actions(ix, cx))
- .on_mouse_down(
- MouseButton::Middle,
- cx.listener(move |this, _event, _window, cx| {
- this.copy_message(ix, cx);
- }),
- )
- .on_double_click(cx.listener({
- move |this, _event, _window, cx| {
- this.reply_to(ix, cx);
- }
- }))
- .hover(|this| this.bg(cx.theme().surface_background))
+ div()
+ .w_full()
+ .pl_2()
+ .border_l_2()
+ .border_color(cx.theme().element_active)
+ .child(
+ div()
+ .flex()
+ .items_center()
+ .justify_between()
+ .child(
+ div()
+ .flex()
+ .items_baseline()
+ .gap_1()
+ .text_xs()
+ .text_color(cx.theme().text_muted)
+ .child(SharedString::new(t!("chat.replying_to_label")))
+ .child(
+ div()
+ .text_color(cx.theme().text_accent)
+ .child(profile.display_name()),
+ ),
+ )
+ .child(
+ Button::new("remove-reply")
+ .icon(IconName::Close)
+ .xsmall()
+ .ghost()
+ .on_click({
+ let id = text.id;
+ cx.listener(move |this, _, _, cx| {
+ this.remove_reply(&id, cx);
+ })
+ }),
+ ),
+ )
+ .child(
+ div()
+ .w_full()
+ .text_sm()
+ .text_ellipsis()
+ .line_clamp(1)
+ .child(text.content.clone()),
+ )
+ } else {
+ div()
+ }
}
- fn render_message_errors(&self, errors: &[SendError], _cx: &Context) -> impl IntoElement {
- let errors = Rc::new(errors.to_owned());
+ fn render_reply_list(
+ &self,
+ _window: &Window,
+ cx: &Context,
+ ) -> impl IntoIterator
- {
+ let mut items = vec![];
- div()
- .id("")
- .flex()
- .items_center()
- .gap_1()
- .text_color(gpui::red())
- .text_xs()
- .italic()
- .child(Icon::new(IconName::Info).small())
- .child(SharedString::new(t!("chat.send_fail")))
- .on_click(move |_, window, cx| {
- let errors = Rc::clone(&errors);
+ for id in self.replies_to.read(cx).iter() {
+ items.push(self.render_reply(id, cx));
+ }
- window.open_modal(cx, move |this, _window, cx| {
- this.title(SharedString::new(t!("chat.logs_title"))).child(
- div()
- .pb_4()
- .flex()
- .flex_col()
- .gap_2()
- .children(errors.iter().map(|error| {
- div()
- .text_sm()
- .child(
- div()
- .flex()
- .items_baseline()
- .gap_1()
- .text_color(cx.theme().text_muted)
- .child(SharedString::new(t!("chat.send_to_label")))
- .child(error.profile.display_name()),
- )
- .child(error.message.clone())
- })),
- )
- });
- })
- }
-
- fn render_border(&self, cx: &Context) -> impl IntoElement {
- div()
- .group_hover("", |this| this.bg(cx.theme().element_active))
- .absolute()
- .left_0()
- .top_0()
- .w(px(2.))
- .h_full()
- .bg(cx.theme().border_transparent)
- }
-
- fn render_actions(&self, ix: usize, cx: &Context) -> impl IntoElement {
- div()
- .group_hover("", |this| this.visible())
- .invisible()
- .absolute()
- .right_4()
- .top_neg_2()
- .shadow_sm()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().border)
- .bg(cx.theme().background)
- .p_0p5()
- .flex()
- .gap_1()
- .children({
- vec![
- Button::new("reply")
- .icon(IconName::Reply)
- .tooltip(t!("chat.reply_button"))
- .small()
- .ghost()
- .on_click(cx.listener(move |this, _event, _window, cx| {
- this.reply_to(ix, cx);
- })),
- Button::new("copy")
- .icon(IconName::Copy)
- .tooltip(t!("chat.copy_message_button"))
- .small()
- .ghost()
- .on_click(cx.listener(move |this, _event, _window, cx| {
- this.copy_message(ix, cx);
- })),
- ]
- })
+ items
}
}
@@ -863,18 +1094,21 @@ impl Focusable for Chat {
}
impl Render for Chat {
- fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
- let entity = cx.entity();
-
+ fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
v_flex()
.image_cache(self.image_cache.clone())
.size_full()
.child(
- list(self.list_state.clone(), move |ix, window, cx| {
- entity.update(cx, |this, cx| {
- this.render_message(ix, window, cx).into_any_element()
- })
- })
+ list(
+ self.list_state.clone(),
+ cx.processor(move |this, ix, window, cx| {
+ if ix == 0 {
+ this.render_announcement(ix, cx).into_any_element()
+ } else {
+ this.render_message(ix, window, cx).into_any_element()
+ }
+ }),
+ )
.flex_1(),
)
.child(
@@ -888,21 +1122,9 @@ impl Render for Chat {
div()
.flex()
.flex_col()
- .when_some(self.attaches.read(cx).as_ref(), |this, urls| {
- this.gap_1p5()
- .children(urls.iter().map(|url| self.render_attach(url, cx)))
- })
- .when_some(self.replies_to.read(cx).as_ref(), |this, messages| {
- this.gap_1p5().children({
- let mut items = vec![];
-
- for message in messages.iter() {
- items.push(self.render_reply_to(message, cx));
- }
-
- items
- })
- })
+ .gap_1p5()
+ .children(self.render_attachment_list(window, cx))
+ .children(self.render_reply_list(window, cx))
.child(
div()
.w_full()
diff --git a/crates/registry/Cargo.toml b/crates/registry/Cargo.toml
index 0d49567..d339a51 100644
--- a/crates/registry/Cargo.toml
+++ b/crates/registry/Cargo.toml
@@ -9,8 +9,6 @@ common = { path = "../common" }
global = { path = "../global" }
settings = { path = "../settings" }
-rust-i18n.workspace = true
-i18n.workspace = true
gpui.workspace = true
nostr.workspace = true
nostr-sdk.workspace = true
diff --git a/crates/registry/src/lib.rs b/crates/registry/src/lib.rs
index ab15afa..98ea4bd 100644
--- a/crates/registry/src/lib.rs
+++ b/crates/registry/src/lib.rs
@@ -20,8 +20,6 @@ use crate::room::Room;
pub mod message;
pub mod room;
-i18n::init!();
-
pub fn init(cx: &mut App) {
Registry::set_global(cx.new(Registry::new), cx);
}
@@ -421,8 +419,8 @@ impl Registry {
}
// Emit the new message to the room
- cx.defer_in(window, |this, window, cx| {
- this.emit_message(event, window, cx);
+ cx.defer_in(window, move |this, _window, cx| {
+ this.emit_message(event, cx);
});
});
diff --git a/crates/registry/src/message.rs b/crates/registry/src/message.rs
index 9c5caba..5ca8371 100644
--- a/crates/registry/src/message.rs
+++ b/crates/registry/src/message.rs
@@ -1,19 +1,11 @@
use std::hash::Hash;
-use std::iter::IntoIterator;
use chrono::{Local, TimeZone};
use gpui::SharedString;
use nostr_sdk::prelude::*;
-use crate::room::SendError;
-
-/// Represents a message in the chat system.
-///
-/// Contains information about the message content, author, creation time,
-/// mentions, replies, and any errors that occurred during sending.
#[derive(Debug, Clone)]
-pub struct Message {
- /// Unique identifier of the message (EventId from nostr_sdk)
+pub struct RenderedMessage {
pub id: EventId,
/// Author's public key
pub author: PublicKey,
@@ -23,138 +15,82 @@ pub struct Message {
pub created_at: Timestamp,
/// List of mentioned public keys in the message
pub mentions: Vec,
- /// List of EventIds this message is replying to
- pub replies_to: Option>,
- /// Any errors that occurred while sending this message
- pub errors: Option>,
+ /// List of event of the message this message is a reply to
+ pub replies_to: Vec,
}
-impl Eq for Message {}
+impl From for RenderedMessage {
+ fn from(inner: Event) -> Self {
+ let mentions = extract_mentions(&inner.content);
+ let replies_to = extract_reply_ids(&inner.tags);
-impl PartialEq for Message {
+ Self {
+ id: inner.id,
+ author: inner.pubkey,
+ content: inner.content.into(),
+ created_at: inner.created_at,
+ mentions,
+ replies_to,
+ }
+ }
+}
+
+impl From for RenderedMessage {
+ fn from(inner: UnsignedEvent) -> Self {
+ let mentions = extract_mentions(&inner.content);
+ let replies_to = extract_reply_ids(&inner.tags);
+
+ Self {
+ // Event ID must be known
+ id: inner.id.unwrap(),
+ author: inner.pubkey,
+ content: inner.content.into(),
+ created_at: inner.created_at,
+ mentions,
+ replies_to,
+ }
+ }
+}
+
+impl From> for RenderedMessage {
+ fn from(inner: Box) -> Self {
+ (*inner).into()
+ }
+}
+
+impl From<&Box> for RenderedMessage {
+ fn from(inner: &Box) -> Self {
+ inner.to_owned().into()
+ }
+}
+
+impl Eq for RenderedMessage {}
+
+impl PartialEq for RenderedMessage {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
-impl Ord for Message {
+impl Ord for RenderedMessage {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.created_at.cmp(&other.created_at)
}
}
-impl PartialOrd for Message {
+impl PartialOrd for RenderedMessage {
fn partial_cmp(&self, other: &Self) -> Option {
Some(self.cmp(other))
}
}
-impl Hash for Message {
+impl Hash for RenderedMessage {
fn hash(&self, state: &mut H) {
self.id.hash(state);
}
}
-/// Builder pattern implementation for constructing Message objects.
-#[derive(Debug)]
-pub struct MessageBuilder {
- id: EventId,
- author: PublicKey,
- content: Option,
- created_at: Option,
- mentions: Vec,
- replies_to: Option>,
- errors: Option>,
-}
-
-impl MessageBuilder {
- /// Creates a new MessageBuilder with default values
- pub fn new(id: EventId, author: PublicKey) -> Self {
- Self {
- id,
- author,
- content: None,
- created_at: None,
- mentions: vec![],
- replies_to: None,
- errors: None,
- }
- }
-
- /// Sets the message content
- pub fn content(mut self, content: impl Into) -> Self {
- self.content = Some(content.into());
- self
- }
-
- /// Sets the creation timestamp
- pub fn created_at(mut self, created_at: Timestamp) -> Self {
- self.created_at = Some(created_at);
- self
- }
-
- /// Adds a single mention to the message
- pub fn mention(mut self, mention: PublicKey) -> Self {
- self.mentions.push(mention);
- self
- }
-
- /// Adds multiple mentions to the message
- pub fn mentions(mut self, mentions: I) -> Self
- where
- I: IntoIterator
- ,
- {
- self.mentions.extend(mentions);
- self
- }
-
- /// Sets a single message this is replying to
- pub fn reply_to(mut self, reply_to: EventId) -> Self {
- self.replies_to = Some(vec![reply_to]);
- self
- }
-
- /// Sets multiple messages this is replying to
- pub fn replies_to(mut self, replies_to: I) -> Self
- where
- I: IntoIterator
- ,
- {
- let replies: Vec = replies_to.into_iter().collect();
- if !replies.is_empty() {
- self.replies_to = Some(replies);
- }
- self
- }
-
- /// Adds errors that occurred during sending
- pub fn errors(mut self, errors: I) -> Self
- where
- I: IntoIterator
- ,
- {
- self.errors = Some(errors.into_iter().collect());
- self
- }
-
- /// Builds the message
- pub fn build(self) -> Result {
- Ok(Message {
- id: self.id,
- author: self.author,
- content: self.content.ok_or("Content is required")?,
- created_at: self.created_at.unwrap_or_else(Timestamp::now),
- mentions: self.mentions,
- replies_to: self.replies_to,
- errors: self.errors,
- })
- }
-}
-
-impl Message {
- /// Creates a new MessageBuilder
- pub fn builder(id: EventId, author: PublicKey) -> MessageBuilder {
- MessageBuilder::new(id, author)
- }
-
+impl RenderedMessage {
/// Returns a human-readable string representing how long ago the message was created
pub fn ago(&self) -> SharedString {
let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) {
@@ -177,3 +113,41 @@ impl Message {
.into()
}
}
+
+fn extract_mentions(content: &str) -> Vec {
+ let parser = NostrParser::new();
+ let tokens = parser.parse(content);
+
+ tokens
+ .filter_map(|token| match token {
+ Token::Nostr(nip21) => match nip21 {
+ Nip21::Pubkey(pubkey) => Some(pubkey),
+ Nip21::Profile(profile) => Some(profile.public_key),
+ _ => None,
+ },
+ _ => None,
+ })
+ .collect::>()
+}
+
+fn extract_reply_ids(inner: &Tags) -> Vec {
+ let mut replies_to = vec![];
+
+ for tag in inner.filter(TagKind::e()) {
+ if let Some(content) = tag.content() {
+ if let Ok(id) = EventId::from_hex(content) {
+ replies_to.push(id);
+ }
+ }
+ }
+
+ for tag in inner.filter(TagKind::q()) {
+ if let Some(content) = tag.content() {
+ if let Ok(id) = EventId::from_hex(content) {
+ replies_to.push(id);
+ }
+ }
+ }
+
+ replies_to
+}
diff --git a/crates/registry/src/room.rs b/crates/registry/src/room.rs
index d55bf18..36b3825 100644
--- a/crates/registry/src/room.rs
+++ b/crates/registry/src/room.rs
@@ -5,12 +5,11 @@ use chrono::{Local, TimeZone};
use common::display::DisplayProfile;
use common::event::EventUtils;
use global::nostr_client;
-use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window};
+use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use smallvec::SmallVec;
-use crate::message::Message;
use crate::Registry;
pub(crate) const NOW: &str = "now";
@@ -20,15 +19,58 @@ pub(crate) const HOURS_IN_DAY: i64 = 24;
pub(crate) const DAYS_IN_MONTH: i64 = 30;
#[derive(Debug, Clone)]
-pub enum RoomSignal {
- NewMessage(Message),
- Refresh,
+pub struct SendReport {
+ pub receiver: PublicKey,
+ pub output: Option