diff --git a/assets/icons/forward.svg b/assets/icons/forward.svg
new file mode 100644
index 0000000..98c932b
--- /dev/null
+++ b/assets/icons/forward.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/reply.svg b/assets/icons/reply.svg
new file mode 100644
index 0000000..53da0cb
--- /dev/null
+++ b/assets/icons/reply.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/chats/src/message.rs b/crates/chats/src/message.rs
index 5ce4933..e3c9125 100644
--- a/crates/chats/src/message.rs
+++ b/crates/chats/src/message.rs
@@ -1,60 +1,152 @@
use chrono::{Local, TimeZone};
use gpui::SharedString;
use nostr_sdk::prelude::*;
+use std::{cell::RefCell, iter::IntoIterator, rc::Rc};
use crate::room::SendError;
-/// # Message
+/// Represents a message in the chat system.
///
-/// Represents a message in the application.
-///
-/// ## Fields
-///
-/// - `id`: The unique identifier for the message
-/// - `content`: The text content of the message
-/// - `author`: Profile information about who created the message
-/// - `mentions`: List of profiles mentioned in the message
-/// - `created_at`: Timestamp when the message was created
+/// Contains information about the message content, author, creation time,
+/// mentions, replies, and any errors that occurred during sending.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
- pub id: EventId,
- pub content: String,
- pub author: Profile,
+ /// Unique identifier of the message (EventId from nostr_sdk)
+ pub id: Option,
+ /// Author profile information
+ pub author: Option,
+ /// The content/text of the message
+ pub content: SharedString,
+ /// When the message was created
pub created_at: Timestamp,
+ /// List of mentioned profiles 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>,
}
-impl Message {
- /// Creates a new message with the provided details
- ///
- /// # Arguments
- ///
- /// * `id` - Unique event identifier
- /// * `content` - Message text content
- /// * `author` - Profile of the message author
- /// * `created_at` - When the message was created
- ///
- /// # Returns
- ///
- /// A new `Message` instance
- pub fn new(id: EventId, content: String, author: Profile, created_at: Timestamp) -> Self {
- Self {
- id,
- content,
- author,
- created_at,
- mentions: vec![],
- errors: None,
- }
+/// Builder pattern implementation for constructing Message objects.
+#[derive(Debug, Default)]
+pub struct MessageBuilder {
+ id: Option,
+ author: Option,
+ content: Option,
+ created_at: Option,
+ mentions: Vec,
+ replies_to: Option>,
+ errors: Option>,
+}
+
+impl MessageBuilder {
+ /// Creates a new MessageBuilder with default values
+ pub fn new() -> Self {
+ Self::default()
}
- /// Formats the message timestamp as a human-readable relative time
- ///
- /// # Returns
- ///
- /// A formatted string like "Today at 12:30 PM", "Yesterday at 3:45 PM",
- /// or a date and time for older messages
+ /// Sets the message ID
+ pub fn id(mut self, id: EventId) -> Self {
+ self.id = Some(id);
+ self
+ }
+
+ /// Sets the message author
+ pub fn author(mut self, author: Profile) -> Self {
+ self.author = Some(author);
+ self
+ }
+
+ /// Sets the message content
+ pub fn content(mut self, content: String) -> Self {
+ self.content = Some(content);
+ 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: Profile) -> 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 wrapped in an Rc>
+ pub fn build_rc(self) -> Result>, String> {
+ self.build().map(|m| Rc::new(RefCell::new(m)))
+ }
+
+ /// Builds the message
+ pub fn build(self) -> Result {
+ Ok(Message {
+ id: self.id,
+ author: self.author,
+ content: self.content.ok_or("Content is required")?.into(),
+ 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() -> MessageBuilder {
+ MessageBuilder::new()
+ }
+
+ /// Converts the message into an Rc>
+ pub fn into_rc(self) -> Rc> {
+ Rc::new(RefCell::new(self))
+ }
+
+ /// Builds a message from a builder and wraps it in Rc
+ pub fn build_rc(builder: MessageBuilder) -> Result>, String> {
+ builder.build().map(|m| Rc::new(RefCell::new(m)))
+ }
+
+ /// 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) {
chrono::LocalResult::Single(time) => time,
@@ -75,89 +167,4 @@ impl Message {
}
.into()
}
-
- /// Adds or replaces mentions in the message
- ///
- /// # Arguments
- ///
- /// * `mentions` - New list of mentioned profiles
- ///
- /// # Returns
- ///
- /// The same message with updated mentions
- pub fn with_mentions(mut self, mentions: impl IntoIterator
- ) -> Self {
- self.mentions.extend(mentions);
- self
- }
-
- /// Adds or replaces errors in the message
- ///
- /// # Arguments
- ///
- /// * `errors` - New list of errors
- ///
- /// # Returns
- ///
- /// The same message with updated errors
- pub fn with_errors(mut self, errors: Vec) -> Self {
- self.errors = Some(errors);
- self
- }
-}
-
-/// # RoomMessage
-///
-/// Represents different types of messages that can appear in a room.
-///
-/// ## Variants
-///
-/// - `User`: A message sent by a user
-/// - `System`: A message generated by the system
-/// - `Announcement`: A special message type used for room announcements
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RoomMessage {
- /// User message
- User(Box),
- /// System message
- System(SharedString),
- /// Only use for UI purposes.
- /// Placeholder will be used for display room announcement
- Announcement,
-}
-
-impl RoomMessage {
- /// Creates a new user message
- ///
- /// # Arguments
- ///
- /// * `message` - The message content
- ///
- /// # Returns
- ///
- /// A `RoomMessage::User` variant
- pub fn user(message: Message) -> Self {
- Self::User(Box::new(message))
- }
-
- /// Creates a new system message
- ///
- /// # Arguments
- ///
- /// * `content` - The system message content
- ///
- /// # Returns
- ///
- /// A `RoomMessage::System` variant
- pub fn system(content: SharedString) -> Self {
- Self::System(content)
- }
-
- /// Creates a new announcement placeholder
- ///
- /// # Returns
- ///
- /// A `RoomMessage::Announcement` variant
- pub fn announcement() -> Self {
- Self::Announcement
- }
}
diff --git a/crates/chats/src/room.rs b/crates/chats/src/room.rs
index 5db3eca..7750f0f 100644
--- a/crates/chats/src/room.rs
+++ b/crates/chats/src/room.rs
@@ -11,7 +11,7 @@ use nostr_sdk::prelude::*;
use crate::{
constants::{DAYS_IN_MONTH, HOURS_IN_DAY, MINUTES_IN_HOUR, NOW, SECONDS_IN_MINUTE},
- message::{Message, RoomMessage},
+ message::Message,
ChatRegistry,
};
@@ -388,7 +388,7 @@ impl Room {
///
/// A Task that resolves to Result, Error> containing
/// all messages for this room
- pub fn load_messages(&self, cx: &App) -> Task, Error>> {
+ pub fn load_messages(&self, cx: &App) -> Task, Error>> {
let client = get_client();
let pubkeys = Arc::clone(&self.members);
let profiles: Vec = pubkeys
@@ -421,11 +421,26 @@ impl Room {
.collect::>();
for event in events.into_iter() {
- let id = event.id;
- let created_at = event.created_at;
let content = event.content.clone();
let tokens = parser.parse(&content);
let mut mentions = vec![];
+ let mut replies_to = vec![];
+
+ for tag in event.tags.filter(TagKind::e()) {
+ if let Some(content) = tag.content() {
+ if let Ok(id) = EventId::from_hex(content) {
+ replies_to.push(id);
+ }
+ }
+ }
+
+ for tag in event.tags.filter(TagKind::q()) {
+ if let Some(content) = tag.content() {
+ if let Ok(id) = EventId::from_hex(content) {
+ replies_to.push(id);
+ }
+ }
+ }
let author = profiles
.iter()
@@ -454,10 +469,17 @@ impl Room {
);
}
- let message = Message::new(id, content, author, created_at).with_mentions(mentions);
- let room_message = RoomMessage::user(message);
-
- messages.push(room_message);
+ if let Ok(message) = Message::builder()
+ .id(event.id)
+ .content(content)
+ .author(author)
+ .created_at(event.created_at)
+ .replies_to(replies_to)
+ .mentions(mentions)
+ .build()
+ {
+ messages.push(message);
+ }
}
Ok(messages)
@@ -477,11 +499,40 @@ impl Room {
/// Processes the event and emits an Incoming to the UI when complete
pub fn emit_message(&self, event: Event, _window: &mut Window, cx: &mut Context) {
let author = ChatRegistry::get_global(cx).profile(&event.pubkey, cx);
- let mentions = extract_mentions(&event.content, cx);
- let message =
- Message::new(event.id, event.content, author, event.created_at).with_mentions(mentions);
- cx.emit(Incoming(message));
+ // Extract all mentions from content
+ let mentions = extract_mentions(&event.content, cx);
+
+ // Extract reply_to if present
+ let mut replies_to = vec![];
+
+ for tag in event.tags.filter(TagKind::e()) {
+ if let Some(content) = tag.content() {
+ if let Ok(id) = EventId::from_hex(content) {
+ replies_to.push(id);
+ }
+ }
+ }
+
+ for tag in event.tags.filter(TagKind::q()) {
+ if let Some(content) = tag.content() {
+ if let Ok(id) = EventId::from_hex(content) {
+ replies_to.push(id);
+ }
+ }
+ }
+
+ if let Ok(message) = Message::builder()
+ .id(event.id)
+ .content(event.content)
+ .author(author)
+ .created_at(event.created_at)
+ .replies_to(replies_to)
+ .mentions(mentions)
+ .build()
+ {
+ cx.emit(Incoming(message));
+ }
}
/// Creates a temporary message for optimistic updates
@@ -499,22 +550,69 @@ impl Room {
///
/// Returns `Some(Message)` containing the temporary message if the current user's profile is available,
/// or `None` if no account is found.
- pub fn create_temp_message(&self, content: &str, cx: &App) -> Option {
- let profile = Account::get_global(cx).profile.clone()?;
- let public_key = profile.public_key();
+ pub fn create_temp_message(
+ &self,
+ content: &str,
+ replies: Option<&Vec>,
+ cx: &App,
+ ) -> Option {
+ let author = Account::get_global(cx).profile.clone()?;
+ let public_key = author.public_key();
let builder = EventBuilder::private_msg_rumor(public_key, content);
+ // Add event reference if it's present (replying to another event)
+ let mut refs = vec![];
+
+ if let Some(replies) = replies {
+ if replies.len() == 1 {
+ refs.push(Tag::event(replies[0].id.unwrap()))
+ } else {
+ for message in replies.iter() {
+ refs.push(Tag::custom(TagKind::q(), vec![message.id.unwrap()]))
+ }
+ }
+ }
+
+ let mut event = if !refs.is_empty() {
+ builder.tags(refs).build(public_key)
+ } else {
+ builder.build(public_key)
+ };
+
// Create a unsigned event to convert to Coop Message
- let mut event = builder.build(public_key);
event.ensure_id();
// Extract all mentions from content
let mentions = extract_mentions(&event.content, cx);
- Some(
- Message::new(event.id.unwrap(), event.content, profile, event.created_at)
- .with_mentions(mentions),
- )
+ // Extract reply_to if present
+ let mut replies_to = vec![];
+
+ for tag in event.tags.filter(TagKind::e()) {
+ if let Some(content) = tag.content() {
+ if let Ok(id) = EventId::from_hex(content) {
+ replies_to.push(id);
+ }
+ }
+ }
+
+ for tag in event.tags.filter(TagKind::q()) {
+ if let Some(content) = tag.content() {
+ if let Ok(id) = EventId::from_hex(content) {
+ replies_to.push(id);
+ }
+ }
+ }
+
+ Message::builder()
+ .id(event.id.unwrap())
+ .content(event.content)
+ .author(author)
+ .created_at(event.created_at)
+ .replies_to(replies_to)
+ .mentions(mentions)
+ .build()
+ .ok()
}
/// Sends a message to all members in the background task
@@ -528,8 +626,14 @@ impl Room {
///
/// A Task that resolves to Result, Error> where the
/// strings contain error messages for any failed sends
- pub fn send_in_background(&self, msg: &str, cx: &App) -> Task, Error>> {
- let content = msg.to_owned();
+ pub fn send_in_background(
+ &self,
+ content: &str,
+ replies: Option<&Vec>,
+ cx: &App,
+ ) -> Task, Error>> {
+ let content = content.to_owned();
+ let replies = replies.cloned();
let subject = self.subject.clone();
let picture = self.picture.clone();
let public_keys = Arc::clone(&self.members);
@@ -551,6 +655,17 @@ impl Room {
})
.collect();
+ // Add event reference if it's present (replying to another event)
+ if let Some(replies) = replies {
+ if replies.len() == 1 {
+ tags.push(Tag::event(replies[0].id.unwrap()))
+ } else {
+ for message in replies.iter() {
+ tags.push(Tag::custom(TagKind::q(), vec![message.id.unwrap()]))
+ }
+ }
+ }
+
// Add subject tag if it's present
if let Some(subject) = subject {
tags.push(Tag::from_standardized(TagStandard::Subject(
diff --git a/crates/coop/src/views/chat.rs b/crates/coop/src/views/chat.rs
index cf91182..e265a2f 100644
--- a/crates/coop/src/views/chat.rs
+++ b/crates/coop/src/views/chat.rs
@@ -1,10 +1,10 @@
-use std::{collections::HashMap, sync::Arc};
+use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
use anyhow::{anyhow, Error};
use async_utility::task::spawn;
use chats::{
- message::{Message, RoomMessage},
- room::Room,
+ message::Message,
+ room::{Room, SendError},
ChatRegistry,
};
use common::{nip96_upload, profile::SharedProfile};
@@ -35,8 +35,6 @@ use ui::{
use crate::views::subject;
-const DESC: &str = "This conversation is private. Only members can see each other's messages.";
-
#[derive(Clone, PartialEq, Eq, Deserialize)]
pub struct ChangeSubject(pub String);
@@ -56,11 +54,12 @@ pub struct Chat {
focus_handle: FocusHandle,
// Chat Room
room: Entity,
- messages: Entity>,
+ messages: Entity>>>,
text_data: HashMap,
list_state: ListState,
// New Message
input: Entity,
+ replies_to: Entity