update backend
This commit is contained in:
+319
-51
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user