group message

This commit is contained in:
2026-06-10 14:37:50 +07:00
parent e812ae05a9
commit 5edc8d311e
3 changed files with 244 additions and 284 deletions

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
pub use actions::*;
use anyhow::{Context as AnyhowContext, Error};
use chat::{ChatRegistry, Message, RenderedMessage, Room, RoomEvent, SendReport, SendStatus};
use chat::{ChatRegistry, Message, Room, RoomEvent, SendReport, SendStatus};
use common::{TimestampExt, coop_cache};
use gpui::prelude::FluentBuilder;
use gpui::{
@@ -38,9 +38,6 @@ use crate::text::RenderedText;
mod actions;
mod text;
const ANNOUNCEMENT: &str =
"This conversation is private. Only members can see each other's messages.";
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
cx.new(|cx| ChatPanel::new(room, window, cx))
}
@@ -101,7 +98,7 @@ impl ChatPanel {
let reports_by_id = cx.new(|_| BTreeMap::new());
// Define list of messages
let messages = BTreeSet::from([Message::system()]);
let messages = BTreeSet::default();
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
// Get room id and name
@@ -476,25 +473,13 @@ impl ChatPanel {
}
/// Get a message by its ID
fn message(&self, id: &EventId) -> Option<&RenderedMessage> {
self.messages.iter().find_map(|msg| {
if let Message::User(rendered) = msg
&& &rendered.id == id
{
return Some(rendered);
}
None
})
fn message(&self, id: &EventId) -> Option<&Message> {
self.messages.iter().find(|msg| &msg.id == id)
}
fn scroll_to(&self, id: EventId) {
if let Some(ix) = self.messages.iter().position(|m| {
if let Message::User(msg) = m {
msg.id == id
} else {
false
}
}) {
/// Scroll to a message by its ID
fn scroll_to(&self, id: &EventId) {
if let Some(ix) = self.messages.iter().position(|msg| &msg.id == id) {
self.list_state.scroll_to_reveal_item(ix);
}
}
@@ -742,9 +727,11 @@ impl ChatPanel {
cx.open_url(&content);
}
fn render_announcement(&self, ix: usize, cx: &Context<Self>) -> AnyElement {
fn render_announcement(&self, cx: &Context<Self>) -> AnyElement {
const MSG: &str =
"This conversation is private. Only members can see each other's messages.";
v_flex()
.id(ix)
.h_40()
.w_full()
.gap_3()
@@ -761,7 +748,7 @@ impl ChatPanel {
.size_12()
.text_color(cx.theme().ghost_element_active),
)
.child(SharedString::from(ANNOUNCEMENT))
.child(MSG)
.into_any_element()
}
@@ -798,6 +785,34 @@ impl ChatPanel {
.into_any_element()
}
fn is_group_start(&self, ix: usize) -> bool {
// 5 minutes
const GROUP_WINDOW: u64 = 300;
if ix == 0 {
return true;
}
let mut iter = self.messages.iter();
if let Some(previous) = iter.nth(ix - 1)
&& let Some(current) = iter.next()
{
if current.author != previous.author {
return true;
}
let gap = current
.created_at
.as_secs()
.saturating_sub(previous.created_at.as_secs());
return gap > GROUP_WINDOW;
}
false
}
fn render_message(
&mut self,
ix: usize,
@@ -805,24 +820,17 @@ impl ChatPanel {
cx: &mut Context<Self>,
) -> AnyElement {
if let Some(message) = self.messages.iter().nth(ix) {
match message {
Message::User(rendered) => {
let persons = PersonRegistry::global(cx);
let text = self
.rendered_texts_by_id
.entry(rendered.id)
.or_insert_with(|| {
RenderedText::new(&rendered.content, &rendered.mentions, &persons, cx)
})
.element(ix.into(), window, cx);
let persons = PersonRegistry::global(cx);
let show_author = self.is_group_start(ix);
let text = self
.rendered_texts_by_id
.entry(message.id)
.or_insert_with(|| {
RenderedText::new(&message.content, &message.mentions, &persons, cx)
})
.element(ix.into(), window, cx);
self.render_text_message(ix, rendered, text, cx)
}
Message::Warning(content, _timestamp) => {
self.render_warning(ix, SharedString::from(content), cx)
}
Message::System(_timestamp) => self.render_announcement(ix, cx),
}
self.render_text_message(ix, message, text, show_author, cx)
} else {
self.render_warning(ix, SharedString::from("Message not found"), cx)
}
@@ -831,8 +839,9 @@ impl ChatPanel {
fn render_text_message(
&self,
ix: usize,
message: &RenderedMessage,
message: &Message,
rendered_text: AnyElement,
show_author: bool,
cx: &Context<Self>,
) -> AnyElement {
let id = message.id;
@@ -858,17 +867,21 @@ impl ChatPanel {
.flex()
.gap_3()
.when(!hide_avatar, |this| {
this.child(
Avatar::new(author.avatar())
.flex_shrink_0()
.relative()
.dropdown_menu(move |this, _window, _cx| {
this.menu("Public Key", Box::new(Command::Copy(pk)))
.menu("View Relays", Box::new(Command::Relays(pk)))
.separator()
.menu("View on njump.me", Box::new(Command::Njump(pk)))
}),
)
if show_author {
this.child(
Avatar::new(author.avatar())
.flex_shrink_0()
.relative()
.dropdown_menu(move |this, _window, _cx| {
this.menu("Public Key", Box::new(Command::Copy(pk)))
.menu("View Relays", Box::new(Command::Relays(pk)))
.separator()
.menu("View on njump.me", Box::new(Command::Njump(pk)))
}),
)
} else {
this.child(div().flex_shrink_0().w(px(32.)))
}
})
.child(
v_flex()
@@ -876,22 +889,24 @@ impl ChatPanel {
.w_full()
.flex_initial()
.overflow_hidden()
.child(
h_flex()
.gap_2()
.text_sm()
.text_color(cx.theme().text_placeholder)
.child(
div()
.font_semibold()
.text_color(cx.theme().text)
.child(author.name()),
)
.child(message.created_at.to_human_time())
.when(has_reports, |this| {
this.child(self.render_sent_reports(&id, cx))
}),
)
.when(show_author, |this| {
this.child(
h_flex()
.gap_2()
.text_sm()
.text_color(cx.theme().text_placeholder)
.child(
div()
.font_semibold()
.text_color(cx.theme().text)
.child(author.name()),
)
.child(message.created_at.to_human_time())
.when(has_reports, |this| {
this.child(self.render_sent_reports(&id, cx))
}),
)
})
.when(has_replies, |this| {
this.children(self.render_message_replies(replies, cx))
})
@@ -1009,7 +1024,7 @@ impl ChatPanel {
.on_click({
let id = *id;
cx.listener(move |this, _event, _window, _cx| {
this.scroll_to(id);
this.scroll_to(&id);
})
}),
);
@@ -1500,15 +1515,28 @@ impl Render for ChatPanel {
v_flex()
.flex_1()
.relative()
.child(
list(
self.list_state.clone(),
cx.processor(move |this, ix, window, cx| {
this.render_message(ix, window, cx)
}),
)
.size_full(),
)
.map(|this| {
if self.messages.is_empty() {
this.child(
div()
.size_full()
.flex()
.items_center()
.justify_end()
.child(self.render_announcement(cx)),
)
} else {
this.child(
list(
self.list_state.clone(),
cx.processor(move |this, ix, window, cx| {
this.render_message(ix, window, cx)
}),
)
.size_full(),
)
}
})
.child(Scrollbar::vertical(&self.list_state)),
)
.child(