add group activity rows to nip29 timeline

This commit is contained in:
2026-10-01 04:43:20 +07:00
parent 41e48de4fd
commit 61aa7c4a70
6 changed files with 579 additions and 99 deletions
+32 -32
View File
@@ -1,7 +1,6 @@
use std::collections::BTreeMap;
use anyhow::Result;
use chat::Message;
use chat_ui::text::rendered_text;
use gpui::prelude::FluentBuilder;
use gpui::{
@@ -9,7 +8,7 @@ use gpui::{
Focusable, FollowMode, IntoElement, ListAlignment, ListScrollEvent, ListState, ParentElement,
Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div, list, px,
};
use nip29::{Group, GroupEvent, GroupMetadata, GroupsRegistry, Membership, Pin};
use nip29::{Group, GroupEvent, GroupMetadata, GroupsRegistry, Membership, Pin, Row};
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{SmallVec, smallvec};
@@ -88,20 +87,23 @@ fn display_name(group: &Group) -> SharedString {
)
}
fn opens_run(messages: &[Message], index: usize) -> bool {
let Some(current) = messages.get(index) else {
fn opens_run(rows: &[Row], index: usize) -> bool {
let (Some(current), Some(previous)) = (
rows.get(index),
index.checked_sub(1).and_then(|index| rows.get(index)),
) else {
return true;
};
let Some(previous) = index.checked_sub(1).and_then(|index| messages.get(index)) else {
if !matches!((current, previous), (Row::Message(_), Row::Message(_))) {
return true;
};
}
current.author != previous.author
current.author() != previous.author()
|| current
.created_at
.created_at()
.as_secs()
.saturating_sub(previous.created_at.as_secs())
.saturating_sub(previous.created_at().as_secs())
> RUN_WINDOW_SECS
}
@@ -123,9 +125,7 @@ pub(crate) fn pin_label(group: &Group, pin: &Pin) -> SharedString {
Pin::Address(coordinate) => SharedString::from(format!("{coordinate}")),
Pin::Event(id) => {
let snippet = group
.messages()
.iter()
.find(|message| message.id == *id)
.message(*id)
.and_then(|message| message.content.lines().next())
.map(str::trim)
.filter(|line| !line.is_empty());
@@ -154,13 +154,13 @@ impl GroupPanel {
pub fn new(group: Entity<Group>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let (id, name, count, head) = {
let group = group.read(cx);
let messages = group.messages();
let rows = group.rows();
(
SharedString::from(format!("group-{}", group.key().cache_tag())),
display_name(group),
messages.len(),
messages.first().map(|message| message.id),
rows.len(),
rows.first().map(Row::id),
)
};
@@ -220,7 +220,7 @@ impl GroupPanel {
fn item_count(&self, cx: &App) -> usize {
self.group
.upgrade()
.map_or(2, |group| group.read(cx).messages().len() + 2)
.map_or(2, |group| group.read(cx).rows().len() + 2)
}
fn sync(&mut self, cx: &App) {
@@ -229,9 +229,9 @@ impl GroupPanel {
};
let group = group.read(cx);
let messages = group.messages();
let head = messages.first().map(|message| message.id);
let total = messages.len() + 2;
let rows = group.rows();
let head = rows.first().map(Row::id);
let total = rows.len() + 2;
let old_total = self.list_state.item_count();
if total == old_total {
@@ -242,8 +242,8 @@ impl GroupPanel {
let old_len = old_total.saturating_sub(2);
let prepended = self
.head
.and_then(|old_head| messages.iter().position(|message| message.id == old_head))
.filter(|prepended| prepended + old_len <= messages.len());
.and_then(|old_head| rows.iter().position(|row| row.id() == old_head))
.filter(|prepended| prepended + old_len <= rows.len());
match prepended {
Some(prepended) => {
@@ -251,7 +251,7 @@ impl GroupPanel {
self.list_state.splice(2..2, prepended);
}
let appended = messages.len() - old_len - prepended;
let appended = rows.len() - old_len - prepended;
if appended > 0 {
let at = old_total + prepended;
@@ -837,12 +837,7 @@ impl GroupPanel {
}
}
fn render_message(
&mut self,
ix: usize,
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
fn render_row(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
if ix == 0 {
return self.render_welcome(cx);
}
@@ -856,12 +851,17 @@ impl GroupPanel {
};
let group = group.read(cx);
let messages = group.messages();
let rows = group.rows();
let Some(message) = messages.get(ix - 2) else {
let Some(row) = rows.get(ix - 2) else {
return div().into_any_element();
};
let message = match row {
Row::Message(message) => message,
Row::Activity(activity) => return message::render_activity(ix, activity, cx),
};
let nostr = NostrRegistry::global(cx);
let persons = PersonRegistry::global(cx);
@@ -878,7 +878,7 @@ impl GroupPanel {
.current_user()
.is_some_and(|me| group.admins().contains(&me));
let show_author = opens_run(messages, ix - 2);
let show_author = opens_run(rows, ix - 2);
let pinned = group.pins().pins().contains(&Pin::Event(message.id));
message::render(
@@ -1005,7 +1005,7 @@ impl Render for GroupPanel {
list(
self.list_state.clone(),
cx.processor(move |this, ix, window, cx| {
this.render_message(ix, window, cx)
this.render_row(ix, window, cx)
}),
)
.size_full(),
+36 -4
View File
@@ -2,11 +2,11 @@ use chat::Message;
use common::TimestampExt;
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, InteractiveElement, IntoElement, ObjectFit, ParentElement, SharedUri, Styled,
StyledImage, WeakEntity, div, img, px,
AnyElement, App, InteractiveElement, IntoElement, ObjectFit, ParentElement, SharedString,
SharedUri, Styled, StyledImage, WeakEntity, div, img, px,
};
use nip29::Group;
use nostr_sdk::prelude::EventId;
use nip29::{Activity, Group};
use nostr_sdk::prelude::{EventId, Kind};
use person::PersonRegistry;
use settings::AppSettings;
use theme::ActiveTheme;
@@ -129,6 +129,38 @@ fn react_menu(
})
}
pub(crate) fn render_activity(ix: usize, activity: &Activity, cx: &App) -> AnyElement {
let persons = PersonRegistry::global(cx);
let subject = persons.read(cx).get(&activity.subject(), cx);
let hide_avatar = AppSettings::get_hide_avatar(cx);
MessageRow::new(ix)
.show_author(true)
.hide_avatar(hide_avatar)
.avatar(
Avatar::new(subject.avatar())
.seed(subject.avatar_seed())
.flex_shrink_0(),
)
.author(SharedString::from(format!(
"{} {}",
subject.name(),
action(activity)
)))
.timestamp(activity.created_at.to_human_time())
.into_any_element()
}
fn action(activity: &Activity) -> String {
match activity.kind {
Kind::GroupJoinRequest => "joined the group".to_owned(),
Kind::GroupLeaveRequest => "left the group".to_owned(),
Kind::GroupRemoveUser => "was removed from the group".to_owned(),
Kind::GroupPutUser => format!("is now {}", activity.roles.join(", ")),
_ => String::new(),
}
}
fn media(media: &[SharedUri], cx: &App) -> AnyElement {
if let [only] = media {
return div()