feat: Reply or Reference a specific message (#39)

* add reply to when send message

* show reply message

* refactor

* multiple quote
This commit is contained in:
reya
2025-05-21 17:44:43 +07:00
committed by GitHub
parent ba42bafc3a
commit 3fd236de73
8 changed files with 712 additions and 407 deletions

View File

@@ -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<EventId>,
/// Author profile information
pub author: Option<Profile>,
/// 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<Profile>,
/// List of EventIds this message is replying to
pub replies_to: Option<Vec<EventId>>,
/// Any errors that occurred while sending this message
pub errors: Option<Vec<SendError>>,
}
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<EventId>,
author: Option<Profile>,
content: Option<String>,
created_at: Option<Timestamp>,
mentions: Vec<Profile>,
replies_to: Option<Vec<EventId>>,
errors: Option<Vec<SendError>>,
}
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<I>(mut self, mentions: I) -> Self
where
I: IntoIterator<Item = Profile>,
{
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<I>(mut self, replies_to: I) -> Self
where
I: IntoIterator<Item = EventId>,
{
let replies: Vec<EventId> = replies_to.into_iter().collect();
if !replies.is_empty() {
self.replies_to = Some(replies);
}
self
}
/// Adds errors that occurred during sending
pub fn errors<I>(mut self, errors: I) -> Self
where
I: IntoIterator<Item = SendError>,
{
self.errors = Some(errors.into_iter().collect());
self
}
/// Builds the message wrapped in an Rc<RefCell<Message>>
pub fn build_rc(self) -> Result<Rc<RefCell<Message>>, String> {
self.build().map(|m| Rc::new(RefCell::new(m)))
}
/// Builds the message
pub fn build(self) -> Result<Message, String> {
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<RefCell<Message>>
pub fn into_rc(self) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(self))
}
/// Builds a message from a builder and wraps it in Rc<RefCell>
pub fn build_rc(builder: MessageBuilder) -> Result<Rc<RefCell<Self>>, 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<Item = Profile>) -> 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<SendError>) -> 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<Message>),
/// 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
}
}

View File

@@ -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<Vec<RoomMessage>, Error> containing
/// all messages for this room
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<RoomMessage>, Error>> {
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<Message>, Error>> {
let client = get_client();
let pubkeys = Arc::clone(&self.members);
let profiles: Vec<Profile> = pubkeys
@@ -421,11 +421,26 @@ impl Room {
.collect::<Vec<_>>();
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<Self>) {
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<Message> {
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<Message>>,
cx: &App,
) -> Option<Message> {
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<Vec<String>, Error> where the
/// strings contain error messages for any failed sends
pub fn send_in_background(&self, msg: &str, cx: &App) -> Task<Result<Vec<SendError>, Error>> {
let content = msg.to_owned();
pub fn send_in_background(
&self,
content: &str,
replies: Option<&Vec<Message>>,
cx: &App,
) -> Task<Result<Vec<SendError>, 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(