update backend

This commit is contained in:
2026-09-22 14:33:41 +07:00
parent 7e7d13cbfc
commit 8914685c3d
17 changed files with 2575 additions and 971 deletions
+319 -51
View File
@@ -1,11 +1,17 @@
use std::collections::{HashMap, HashSet};
use anyhow::Result;
use community::{ChannelId, ChatMessage, Community, CommunityEvent};
use community::{
ChannelId, ChatMessage, Community, CommunityEvent, Intent, LOAD_OLDER_PAGES, TIMELINE_PAGE,
Timeline,
};
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ListAlignment, ListState, ParentElement, Render, SharedString, Styled,
Subscription, Task, WeakEntity, Window, div, list, px,
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, FollowMode,
IntoElement, ListAlignment, ListScrollEvent, ListState, ParentElement, Render, SharedString,
Styled, Subscription, Task, WeakEntity, Window, div, list, px,
};
use nostr_sdk::prelude::EventId;
use smallvec::{SmallVec, smallvec};
use theme::ActiveTheme;
use ui::avatar::Avatar;
@@ -18,6 +24,9 @@ use ui::{IconName, Sizable, WindowExtension, h_flex, v_flex};
mod message;
/// How near the top row a scroll has to come before the panel splices older history in.
const LOAD_OLDER_THRESHOLD: usize = 20;
pub fn init(
community: Entity<Community>,
window: &mut Window,
@@ -30,25 +39,20 @@ pub fn init(
pub struct CommunityPanel {
id: SharedString,
focus_handle: FocusHandle,
/// Community
community: WeakEntity<Community>,
/// The selected channel
channel: Option<ChannelId>,
/// The selected channel's timeline (oldest first)
messages: Vec<ChatMessage>,
rows: Vec<ChatMessage>,
/// Whether the store holds rows older than `rows`
has_more: bool,
/// A round or a page read is in flight
loading: bool,
/// Message list state
list_state: ListState,
/// Message input state
input: Entity<TextareaState>,
/// Async operations
tasks: Vec<Task<Result<()>>>,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 2]>,
}
@@ -86,7 +90,8 @@ impl CommunityPanel {
&community,
window,
|_this, _community, event, window, cx| match event {
// The fold holds the community, so reload once it is released.
// The fold holds the community, and a round caches rows nothing
// has read yet, so re-read once either is released.
CommunityEvent::Updated(_) => {
cx.defer_in(window, |this, window, cx| this.reload(window, cx));
}
@@ -107,14 +112,26 @@ impl CommunityPanel {
focus_handle: cx.focus_handle(),
community: community.downgrade(),
channel,
messages: Vec::new(),
rows: Vec::new(),
has_more: false,
loading: false,
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.)),
input,
tasks: Vec::new(),
_subscriptions: subscriptions,
};
cx.defer_in(window, |this, window, cx| this.load(window, cx));
panel.list_state.set_follow_mode(FollowMode::Tail);
panel.list_state.set_scroll_handler(cx.listener(
|this, event: &ListScrollEvent, window, cx| {
if event.visible_range.start <= LOAD_OLDER_THRESHOLD {
this.load_older(window, cx);
}
},
));
cx.defer_in(window, |this, window, cx| {
this.load(window, cx);
});
panel
}
@@ -129,60 +146,104 @@ impl CommunityPanel {
if channel != self.channel {
self.channel = channel;
self.messages.clear();
self.list_state.reset(0);
self.rows.clear();
self.has_more = false;
self.loading = false;
self.list_state.reset(1);
}
channel
}
/// Page the selected channel's history into the cache, then read it back.
/// The list's item count: the load-older row, then every message row.
fn item_count(&self) -> usize {
self.rows.len() + 1
}
/// A timeline read, `before_ms` exclusive, or `None` for the newest rows.
fn read(
&self,
channel: ChannelId,
before_ms: Option<u64>,
cx: &App,
) -> Option<Task<Result<Timeline>>> {
self.community
.read_with(cx, |community, cx| {
community.timeline(&channel, before_ms, TIMELINE_PAGE, cx)
})
.ok()
}
/// A channel round, or `None` once the community is gone.
fn sync(
&self,
channel: ChannelId,
intent: Intent,
cx: &mut App,
) -> Option<Task<Result<community::Progress>>> {
self.community
.update(cx, |community, cx| {
community.sync_channel(&channel, intent, cx)
})
.ok()
}
/// Paint the selected channel's cache, then catch it up from the relays.
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(channel) = self.resolve_channel(cx) else {
return;
};
let Ok(backfill) = self
.community
.read_with(cx, |community, cx| community.backfill(&channel, cx))
else {
self.reload(window, cx);
self.round(channel, Intent::CatchUp, window, cx);
}
/// Run a round for `channel`; its completion re-reads the timeline.
fn round(
&mut self,
channel: ChannelId,
intent: Intent,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(round) = self.sync(channel, intent, cx) else {
return;
};
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
if let Err(error) = backfill.await {
log::warn!("community panel: backfill failed: {error}");
}
self.loading = true;
this.update_in(cx, |this, window, cx| this.reload(window, cx))?;
cx.spawn_in::<_, Result<()>>(window, async move |this, cx| {
let result = round.await;
this.update_in(cx, |this, window, cx| {
this.loading = false;
if let Err(error) = result {
window.push_notification(
Notification::error(error.to_string()).autohide(false),
cx,
);
}
})?;
Ok(())
}));
})
.detach();
}
/// Replace the timeline with the selected channel's folded messages.
/// Read the newest page and fold it into what is on screen.
fn reload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(channel) = self.resolve_channel(cx) else {
return;
};
let Ok(messages) = self
.community
.read_with(cx, |community, cx| community.messages(&channel, cx))
else {
let Some(timeline) = self.read(channel, None, cx) else {
return;
};
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
match messages.await {
Ok(messages) => {
this.update(cx, |this, cx| {
this.messages = messages;
this.list_state.reset(this.messages.len());
this.list_state.scroll_to_end();
cx.notify();
})?;
}
cx.spawn_in::<_, Result<()>>(window, async move |this, cx| {
match timeline.await {
Ok(timeline) => this.update(cx, |this, cx| this.apply(channel, timeline, cx))?,
Err(error) => {
this.update_in(cx, |_this, window, cx| {
window.push_notification(
@@ -194,7 +255,183 @@ impl CommunityPanel {
}
Ok(())
}));
})
.detach();
}
/// Splice the page of history above the oldest row on screen.
fn load_older(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.loading || !self.has_more {
return;
}
let Some(channel) = self.channel else {
return;
};
let Some(before_ms) = self
.rows
.first()
.map(|message| message.at_ms.saturating_sub(1))
else {
return;
};
let Some(page) = self.read(channel, Some(before_ms), cx) else {
return;
};
self.loading = true;
cx.spawn_in::<_, Result<()>>(window, async move |this, cx| {
let timeline = match page.await {
Ok(timeline) => timeline,
Err(error) => {
this.update_in(cx, |this, window, cx| {
this.loading = false;
window.push_notification(
Notification::error(error.to_string()).autohide(false),
cx,
);
})?;
return Ok(());
}
};
let swept = this.update(cx, |this, cx| {
this.prepend(channel, timeline, cx);
!this.has_more
})?;
if !swept {
this.update(cx, |this, _cx| this.loading = false)?;
return Ok(());
}
let round = this.update(cx, |this, cx| {
this.sync(
channel,
Intent::Older {
pages: LOAD_OLDER_PAGES,
},
cx,
)
})?;
let Some(round) = round else {
this.update(cx, |this, _cx| this.loading = false)?;
return Ok(());
};
match round.await {
Ok(_) => {
let page =
this.update(cx, |this, cx| this.read(channel, Some(before_ms), cx))?;
if let Some(page) = page {
match page.await {
Ok(timeline) => {
this.update(cx, |this, cx| this.prepend(channel, timeline, cx))?
}
Err(error) => this.update_in(cx, |_this, window, cx| {
window.push_notification(
Notification::error(error.to_string()).autohide(false),
cx,
);
})?,
}
}
this.update(cx, |this, _cx| this.loading = false)?;
}
Err(error) => {
this.update_in(cx, |this, window, cx| {
this.loading = false;
window.push_notification(
Notification::error(error.to_string()).autohide(false),
cx,
);
})?;
}
}
Ok(())
})
.detach();
}
/// Fold a freshly read window into the rows on screen.
fn apply(&mut self, channel: ChannelId, timeline: Timeline, cx: &mut Context<Self>) {
if self.channel != Some(channel) {
return;
}
let Timeline { messages, has_more } = timeline;
let connected = self
.rows
.last()
.is_some_and(|last| messages.iter().any(|message| message.id == last.id));
if !connected {
self.rows = messages;
self.has_more = has_more;
self.list_state.reset(self.item_count());
cx.notify();
return;
}
let mut index: HashMap<EventId, usize> = self
.rows
.iter()
.enumerate()
.map(|(ix, message)| (message.id, ix))
.collect();
let mut added = Vec::new();
for message in messages {
match index.get(&message.id).copied() {
Some(ix) => self.rows[ix] = message,
None => {
index.insert(message.id, self.rows.len() + added.len());
added.push(message);
}
}
}
if !added.is_empty() {
let at = self.item_count();
let count = added.len();
self.rows.extend(added);
self.list_state.splice(at..at, count);
}
cx.notify();
}
/// Splice older rows in above what is on screen.
fn prepend(&mut self, channel: ChannelId, timeline: Timeline, cx: &mut Context<Self>) {
if self.channel != Some(channel) {
return;
}
let known: HashSet<EventId> = self.rows.iter().map(|message| message.id).collect();
let added: Vec<ChatMessage> = timeline
.messages
.into_iter()
.filter(|message| !known.contains(&message.id))
.collect();
self.has_more = timeline.has_more;
if !added.is_empty() {
let count = added.len();
self.rows.splice(0..0, added);
self.list_state.splice(1..1, count);
}
cx.notify();
}
fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -224,7 +461,7 @@ impl CommunityPanel {
input.set_value("", window, cx);
});
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
cx.spawn_in::<_, Result<()>>(window, async move |this, cx| {
match send.await {
Ok(_) => {
this.update_in(cx, |this, window, cx| this.reload(window, cx))?;
@@ -240,7 +477,33 @@ impl CommunityPanel {
}
Ok(())
}));
})
.detach();
}
/// The row at index 0: the affordance that pages older history in.
fn render_older(&self, cx: &mut Context<Self>) -> AnyElement {
if !self.has_more {
return div().into_any_element();
}
h_flex()
.w_full()
.justify_center()
.py_2()
.child(
Button::new("load-older")
.label(if self.loading {
"Loading earlier messages…"
} else {
"Load earlier messages"
})
.ghost()
.small()
.loading(self.loading)
.on_click(cx.listener(|this, _event, window, cx| this.load_older(window, cx))),
)
.into_any_element()
}
fn render_message(
@@ -249,9 +512,14 @@ impl CommunityPanel {
_window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
let Some(message) = self.messages.get(ix) else {
if ix == 0 {
return self.render_older(cx);
}
let Some(message) = self.rows.get(ix - 1) else {
return div().into_any_element();
};
message::render(ix, message, cx)
}
@@ -323,7 +591,7 @@ impl Render for CommunityPanel {
.min_h_0()
.relative()
.map(|this| {
if self.messages.is_empty() {
if self.rows.is_empty() {
this.child(
h_flex()
.size_full()