add timeline paging for older messages

This commit is contained in:
2026-09-30 12:49:37 +07:00
parent de862637f3
commit d621b125a9
4 changed files with 164 additions and 6 deletions
+64 -2
View File
@@ -1,4 +1,5 @@
use std::collections::{HashMap, HashSet};
use std::time::Duration;
use anyhow::{Result, anyhow, bail};
use chat::Message;
@@ -14,6 +15,8 @@ use crate::protocol::{
};
const REPLAY_LIMIT: usize = 100;
const PAGE_LIMIT: usize = 50;
const PAGE_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Absorbed {
@@ -44,7 +47,9 @@ pub struct Group {
messages: Vec<Message>,
index: HashSet<EventId>,
window: TimelineWindow,
oldest: Option<Timestamp>,
loading: bool,
loading_more: bool,
refused: Option<String>,
tasks: SmallVec<[Task<()>; 2]>,
}
@@ -66,7 +71,9 @@ impl Group {
messages: Vec::new(),
index: HashSet::new(),
window: TimelineWindow::new(),
oldest: None,
loading: true,
loading_more: false,
refused: None,
tasks: smallvec![],
}
@@ -108,6 +115,14 @@ impl Group {
self.loading
}
pub fn loading_more(&self) -> bool {
self.loading_more
}
pub fn oldest(&self) -> Option<Timestamp> {
self.oldest
}
pub fn refused(&self) -> Option<&str> {
self.refused.as_deref()
}
@@ -269,6 +284,45 @@ impl Group {
}));
}
pub(crate) fn load_page(&mut self, cx: &mut Context<Self>) {
let Some(until) = self.oldest else {
return;
};
self.loading_more = true;
cx.notify();
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let id = crate::page_subscription(&self.key);
let relay = self.key.relay().clone();
let filter = self.key.id().timeline_page(PAGE_LIMIT, until);
self.tasks.push(cx.background_spawn(async move {
client.add_relay(relay.clone()).and_connect().await.ok();
let opts = SubscribeAutoCloseOptions::default()
.exit_policy(ReqExitPolicy::ExitOnEOSE)
.timeout(Some(PAGE_TIMEOUT));
if let Err(error) = client
.subscribe(ReqTarget::single(relay, [filter]))
.with_id(id)
.close_on(opts)
.await
{
log::warn!("nip29: loading older messages failed: {error}");
}
}));
}
pub(crate) fn page_finished(&mut self, cx: &mut Context<Self>) {
self.loading_more = false;
cx.emit(GroupEvent::Updated);
cx.notify();
}
pub(crate) fn save_envelope(&self, event: Event, cx: &App) -> Task<Result<()>> {
let cache_tag = self.key.cache_tag();
let nostr = NostrRegistry::global(cx);
@@ -366,7 +420,7 @@ impl Group {
}
if MEMBERSHIP_KINDS.contains(&event.kind) {
self.window.push(event.clone());
self.track(event);
if !event.tags.public_keys().any(|key| key == me) {
return Absorbed::Ignored;
@@ -387,7 +441,7 @@ impl Group {
};
}
self.window.push(event.clone());
self.track(event);
if RENDER_KINDS.contains(&event.kind) {
Absorbed::Rendered {
@@ -398,6 +452,14 @@ impl Group {
}
}
fn track(&mut self, event: &Event) {
self.window.push(event.clone());
if self.oldest.is_none_or(|oldest| event.created_at < oldest) {
self.oldest = Some(event.created_at);
}
}
fn absorb_state(&mut self, event: &Event) -> bool {
match event.kind {
Kind::GroupMetadata => match GroupMetadata::parse(event) {
+63 -3
View File
@@ -21,6 +21,7 @@ pub(crate) static LOCAL_KEYS: LazyLock<Keys> = LazyLock::new(Keys::generate);
const LIST_SUBSCRIPTION: &str = "nip29-list";
const GROUP_PREFIX: &str = "grp-";
const BROWSE_PREFIX: &str = "brw-";
const PAGE_PREFIX: &str = "pg-";
const BROWSE_LIMIT: usize = 500;
const BROWSE_TIMEOUT: Duration = Duration::from_secs(10);
@@ -67,9 +68,20 @@ fn browse_subscription(relay: &RelayUrl) -> SubscriptionId {
SubscriptionId::new(format!("{BROWSE_PREFIX}{:08x}", hasher.finish() as u32))
}
fn page_subscription(key: &GroupKey) -> SubscriptionId {
SubscriptionId::new(format!("{PAGE_PREFIX}{:08x}", key.uniq_id() as u32))
}
fn is_page(id: &SubscriptionId) -> bool {
id.as_str().starts_with(PAGE_PREFIX)
}
fn is_own_subscription(id: &SubscriptionId) -> bool {
let id = id.as_str();
id == LIST_SUBSCRIPTION || id.starts_with(GROUP_PREFIX) || id.starts_with(BROWSE_PREFIX)
id == LIST_SUBSCRIPTION
|| id.starts_with(GROUP_PREFIX)
|| id.starts_with(BROWSE_PREFIX)
|| id.starts_with(PAGE_PREFIX)
}
impl EventEmitter<GroupsEvent> for GroupsRegistry {}
@@ -259,6 +271,22 @@ impl GroupsRegistry {
self.browse.get(&browse_subscription(relay))
}
pub fn load_more(&mut self, key: &GroupKey, cx: &mut Context<Self>) {
let Some(group) = self.group(key, cx) else {
return;
};
if group.read(cx).loading_more() || group.read(cx).oldest().is_none() {
return;
}
self.routes.insert(page_subscription(key), key.clone());
group.update(cx, |group, cx| {
group.load_page(cx);
});
}
pub fn leave(&mut self, key: &GroupKey, cx: &mut Context<Self>) {
let Some(group) = self.group(key, cx) else {
return;
@@ -394,9 +422,11 @@ impl GroupsRegistry {
fn forget(&mut self, key: GroupKey, cx: &mut Context<Self>) {
let id = subscription_id(&key);
let page = page_subscription(&key);
let cache_tag = key.cache_tag();
self.routes.remove(&id);
self.routes.remove(&page);
self.observers.remove(&key);
self.groups.retain(|group| group.read(cx).key() != &key);
self.list.remove(&key);
@@ -405,8 +435,10 @@ impl GroupsRegistry {
let client = nostr.read(cx).client();
self.tasks.push(cx.background_spawn(async move {
if let Err(error) = client.unsubscribe(&id).await {
log::warn!("nip29: unsubscribing {id} failed: {error}");
for id in [id, page] {
if let Err(error) = client.unsubscribe(&id).await {
log::warn!("nip29: unsubscribing {id} failed: {error}");
}
}
let filter = Filter::new()
@@ -595,6 +627,18 @@ impl GroupsRegistry {
return;
};
if is_page(&id) {
self.routes.remove(&id);
if let Some(group) = self.group(&key, cx) {
group.update(cx, |group, cx| {
group.page_finished(cx);
});
}
return;
}
let Some(group) = self.group(&key, cx) else {
return;
};
@@ -620,6 +664,22 @@ impl GroupsRegistry {
return;
};
if is_page(&id) {
self.routes.remove(&id);
log::warn!(
"nip29: loading older messages in {} was refused: {reason}",
key.id()
);
if let Some(group) = self.group(&key, cx) {
group.update(cx, |group, cx| {
group.page_finished(cx);
});
}
return;
}
let Some(group) = self.group(&key, cx) else {
return;
};
+4
View File
@@ -71,6 +71,10 @@ impl GroupId {
.limit(limit)
}
pub fn timeline_page(&self, limit: usize, until: Timestamp) -> Filter {
self.timeline_filter(limit).until(until)
}
pub fn state_filter(&self) -> Filter {
Filter::new().identifier(self.0.clone()).kinds(STATE_KINDS)
}