feat: add community ui #52

Merged
reya merged 48 commits from feat/community-ui into master 2026-09-23 01:12:53 +00:00
3 changed files with 49 additions and 33 deletions
Showing only changes of commit 7ca6634d69 - Show all commits
+22 -8
View File
@@ -27,10 +27,9 @@ mod message;
/// How near the top row a scroll has to come before the panel splices older history in. /// How near the top row a scroll has to come before the panel splices older history in.
const LOAD_OLDER_THRESHOLD: usize = 20; const LOAD_OLDER_THRESHOLD: usize = 20;
/// A repeat message within this window keeps its run, so it carries no avatar or name.
const RUN_WINDOW_MS: u64 = 300_000;
/// Why a room is not showing the messages it knows about.
///
/// "No messages yet" is a claim, and a room we cannot read is not an empty one.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Notice { enum Notice {
Stranded, Stranded,
@@ -59,23 +58,22 @@ impl fmt::Display for Notice {
), ),
Notice::MissingKey(epoch) => write!( Notice::MissingKey(epoch) => write!(
formatter, formatter,
"Messages here can't be read yet — this channel's key for epoch {} is missing", "Messages here can't be read yet, this channel's key for epoch {} is missing",
epoch.0 epoch.0
), ),
Notice::Unreachable => formatter.write_str("Couldn't reach the community's relays"), Notice::Unreachable => formatter.write_str("Couldn't reach the community's relays"),
Notice::Unreadable(1) => { Notice::Unreadable(1) => {
formatter.write_str("1 message here can't be read yet — no key we hold opens it") formatter.write_str("1 message here can't be read yet, no key we hold opens it")
} }
Notice::Unreadable(count) => write!( Notice::Unreadable(count) => write!(
formatter, formatter,
"{count} messages here can't be read yet — no key we hold opens them" "{count} messages here can't be read yet, no key we hold opens them"
), ),
} }
} }
} }
impl Notice { impl Notice {
/// Whether the room still accepts writes, rather than only being readable.
fn writable(self) -> bool { fn writable(self) -> bool {
matches!(self, Notice::Unreachable | Notice::Unreadable(_)) matches!(self, Notice::Unreachable | Notice::Unreadable(_))
} }
@@ -653,6 +651,20 @@ impl CommunityPanel {
.into_any_element() .into_any_element()
} }
/// Whether the row at `index` opens a run from one author.
fn opens_run(&self, index: usize) -> bool {
let Some(current) = self.rows.get(index) else {
return true;
};
let Some(previous) = index.checked_sub(1).and_then(|index| self.rows.get(index)) else {
return true;
};
current.author != previous.author
|| current.at_ms.saturating_sub(previous.at_ms) > RUN_WINDOW_MS
}
fn render_message( fn render_message(
&mut self, &mut self,
ix: usize, ix: usize,
@@ -667,7 +679,9 @@ impl CommunityPanel {
return div().into_any_element(); return div().into_any_element();
}; };
message::render(ix, message, cx) let show_author = self.opens_run(ix - 1);
message::render(ix, message, show_author, cx)
} }
fn render_composer(&self, cx: &mut Context<Self>) -> impl IntoElement { fn render_composer(&self, cx: &mut Context<Self>) -> impl IntoElement {
+12 -11
View File
@@ -4,7 +4,7 @@ use common::TimestampExt;
use community::ChatMessage; use community::ChatMessage;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
AnyElement, App, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div, AnyElement, App, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div, px,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::PersonRegistry;
@@ -13,7 +13,7 @@ use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::{StyledExt, h_flex, v_flex}; use ui::{StyledExt, h_flex, v_flex};
pub(crate) fn render(ix: usize, message: &ChatMessage, cx: &App) -> AnyElement { pub(crate) fn render(ix: usize, message: &ChatMessage, show_author: bool, cx: &App) -> AnyElement {
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 hide_avatar = AppSettings::get_hide_avatar(cx); let hide_avatar = AppSettings::get_hide_avatar(cx);
@@ -29,35 +29,36 @@ pub(crate) fn render(ix: usize, message: &ChatMessage, cx: &App) -> AnyElement {
.items_start() .items_start()
.gap_3() .gap_3()
.when(!hide_avatar, |this| { .when(!hide_avatar, |this| {
if show_author {
this.child( this.child(
Avatar::new(author.avatar()) Avatar::new(author.avatar())
.seed(author.avatar_seed()) .seed(author.avatar_seed())
.flex_shrink_0(), .flex_shrink_0(),
) )
} else {
this.child(div().flex_shrink_0().w(px(32.)))
}
}) })
.child( .child(
v_flex() v_flex()
.flex_1() .flex_1()
.min_w_0() .min_w_0()
.gap_0p5() .gap_0p5()
.child( .when(show_author, |this| {
this.child(
h_flex() h_flex()
.gap_2() .gap_2()
.text_sm() .text_sm()
.text_color(cx.theme().text_placeholder)
.child(div().font_semibold().child(author.name())) .child(div().font_semibold().child(author.name()))
.child( .child(
div() Timestamp::from_secs(message.at_ms / 1000).to_human_time(),
.text_color(cx.theme().text_placeholder)
.child(Timestamp::from_secs(message.at_ms / 1000).to_ago()),
) )
.when(message.edited_at.is_some(), |this| { .when(message.edited_at.is_some(), |this| {
this.child( this.child(div().child("(edited)"))
div()
.text_color(cx.theme().text_placeholder)
.child("(edited)"),
)
}), }),
) )
})
.child(content(message, cx)) .child(content(message, cx))
.when(!message.reactions.is_empty(), |this| { .when(!message.reactions.is_empty(), |this| {
this.child(reactions(message, cx)) this.child(reactions(message, cx))
+2 -1
View File
@@ -129,7 +129,6 @@ pub fn build_message(
build_rumor_ms(KIND_MESSAGE, author, content, tags, at_ms) build_rumor_ms(KIND_MESSAGE, author, content, tags, at_ms)
} }
/// `parent` is the immediate parent; a `None` root means the parent is the thread's root.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn build_comment( pub fn build_comment(
author: PublicKey, author: PublicKey,
@@ -146,12 +145,14 @@ pub fn build_comment(
tags.push(Tag::custom(TAG_ROOT_KIND, [root.kind.to_string()])); tags.push(Tag::custom(TAG_ROOT_KIND, [root.kind.to_string()]));
tags.push(reply_tag(TAG_ROOT, &root.reply)); tags.push(reply_tag(TAG_ROOT, &root.reply));
if let Some(root_author) = root.reply.author { if let Some(root_author) = root.reply.author {
tags.push(Tag::custom(TAG_ROOT_AUTHOR, [root_author.to_hex()])); tags.push(Tag::custom(TAG_ROOT_AUTHOR, [root_author.to_hex()]));
} }
tags.push(Tag::custom(TAG_TARGET_KIND, [parent.kind.to_string()])); tags.push(Tag::custom(TAG_TARGET_KIND, [parent.kind.to_string()]));
tags.push(reply_tag(TAG_TARGET, &parent.reply)); tags.push(reply_tag(TAG_TARGET, &parent.reply));
if let Some(parent_author) = parent.reply.author { if let Some(parent_author) = parent.reply.author {
tags.push(Tag::custom(TAG_TARGET_AUTHOR, [parent_author.to_hex()])); tags.push(Tag::custom(TAG_TARGET_AUTHOR, [parent_author.to_hex()]));
} }