chore: improve message reply in group chat

This commit is contained in:
2026-10-11 15:02:32 +07:00
parent f8032b2074
commit 05f934c93a
3 changed files with 68 additions and 50 deletions
+1 -3
View File
@@ -1755,9 +1755,7 @@ impl ChatPanel {
.border_l_2() .border_l_2()
.border_color(cx.theme().element_active) .border_color(cx.theme().element_active)
.child( .child(
div() h_flex()
.flex()
.items_center()
.justify_between() .justify_between()
.child( .child(
div() div()
+13
View File
@@ -455,6 +455,19 @@ impl GroupPanel {
}); });
} }
fn scroll_to(&self, id: &EventId, cx: &App) {
let Some(group) = self.group.upgrade() else {
return;
};
let rows = group.read(cx).rows();
if let Some(ix) = rows.iter().position(|row| row.id() == *id) {
// The welcome and load-older rows precede the messages.
self.list_state.scroll_to_reveal_item(ix + 2);
}
}
fn start_reply(&mut self, id: EventId, window: &mut Window, cx: &mut Context<Self>) { fn start_reply(&mut self, id: EventId, window: &mut Window, cx: &mut Context<Self>) {
self.reply_to = Some(id); self.reply_to = Some(id);
self.input.update(cx, |input, cx| { self.input.update(cx, |input, cx| {
+54 -47
View File
@@ -5,7 +5,7 @@ use common::TimestampExt;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
AnyElement, App, InteractiveElement, IntoElement, ObjectFit, ParentElement, SharedString, AnyElement, App, InteractiveElement, IntoElement, ObjectFit, ParentElement, SharedString,
SharedUri, Styled, StyledImage, WeakEntity, div, img, px, SharedUri, StatefulInteractiveElement, Styled, StyledImage, WeakEntity, div, img, px,
}; };
use nip29::{Activity, Group, Reaction}; use nip29::{Activity, Group, Reaction};
use nostr_sdk::prelude::{EventId, Kind, PublicKey}; use nostr_sdk::prelude::{EventId, Kind, PublicKey};
@@ -52,8 +52,13 @@ pub(crate) fn render(
) )
.author(author.name()) .author(author.name())
.timestamp(message.created_at.to_human_time()) .timestamp(message.created_at.to_human_time())
.when_some(message.replies_to.first().copied(), |this, target| { .when(!message.replies_to.is_empty(), |this| {
this.child(reply_context(target, &actions.group, cx)) this.children(replies(
&message.replies_to,
&actions.group,
&actions.panel,
cx,
))
}) })
.child(content) .child(content)
.when(!message.media.is_empty(), |this| { .when(!message.media.is_empty(), |this| {
@@ -258,56 +263,58 @@ fn reaction_bar(reactions: &[Reaction], cx: &App) -> AnyElement {
.into_any_element() .into_any_element()
} }
fn reply_context(target: EventId, group: &WeakEntity<Group>, cx: &App) -> AnyElement { fn replies(
let context = group.upgrade().and_then(|entity| { targets: &[EventId],
entity.read_with(cx, |group, cx| { group: &WeakEntity<Group>,
let message = group.message(target)?; panel: &WeakEntity<GroupPanel>,
cx: &App,
) -> Vec<AnyElement> {
let Some(entity) = group.upgrade() else {
return Vec::new();
};
let mut items = Vec::with_capacity(targets.len());
for (ix, id) in targets.iter().enumerate() {
let context = entity.read_with(cx, |group, cx| {
let message = group.message(*id)?;
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let author = persons.read(cx).get(&message.author, cx); let author = persons.read(cx).get(&message.author, cx);
let line = message.content.lines().next().unwrap_or_default().trim();
Some(( Some((author.name(), message.preview()))
author.name(), });
SharedString::from(line.chars().take(80).collect::<String>()),
))
})
});
let (author, snippet) = context.unwrap_or_else(|| { let (author, preview) = context.unwrap_or_else(|| {
( (
SharedString::from("Message"), SharedString::from("Message"),
SharedString::from("original not loaded"), SharedString::from("original not loaded"),
) )
}); });
h_flex() let id = *id;
.min_w_0() let panel = panel.clone();
.gap_1()
.items_center() items.push(
.child(
Icon::new(IconName::Reply)
.small()
.text_color(cx.theme().text_faint),
)
.child(
div() div()
.flex_shrink_0() .id(ix)
.max_w(px(120.)) .w_full()
.truncate() .px_2()
.font_semibold() .border_l_2()
.text_style(TextStyle::Caption) .border_color(cx.theme().element_active)
.text_color(cx.theme().text_muted) .text_style(TextStyle::Callout)
.child(author), .child(div().font_semibold().child(author))
) .child(div().w_full().text_ellipsis().line_clamp(1).child(preview))
.child( .hover(|this| this.bg(cx.theme().surface_raised))
div() .on_click(move |_event, _window, cx| {
.min_w_0() let _ = panel.update(cx, |panel, cx| {
.truncate() panel.scroll_to(&id, cx);
.text_style(TextStyle::Caption) });
.text_color(cx.theme().text_faint) })
.child(snippet), .into_any_element(),
) );
.into_any_element() }
items
} }
fn media(media: &[SharedUri], cx: &App) -> AnyElement { fn media(media: &[SharedUri], cx: &App) -> AnyElement {