lazy-load group content on first open
This commit is contained in:
@@ -88,6 +88,7 @@ pub struct Group {
|
|||||||
oldest: Option<Timestamp>,
|
oldest: Option<Timestamp>,
|
||||||
loading: bool,
|
loading: bool,
|
||||||
loading_more: bool,
|
loading_more: bool,
|
||||||
|
active: bool,
|
||||||
refused: Option<String>,
|
refused: Option<String>,
|
||||||
tasks: SmallVec<[Task<()>; 2]>,
|
tasks: SmallVec<[Task<()>; 2]>,
|
||||||
}
|
}
|
||||||
@@ -115,6 +116,7 @@ impl Group {
|
|||||||
oldest: None,
|
oldest: None,
|
||||||
loading: true,
|
loading: true,
|
||||||
loading_more: false,
|
loading_more: false,
|
||||||
|
active: false,
|
||||||
refused: None,
|
refused: None,
|
||||||
tasks: smallvec![],
|
tasks: smallvec![],
|
||||||
}
|
}
|
||||||
@@ -196,6 +198,14 @@ impl Group {
|
|||||||
self.loading_more
|
self.loading_more
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_active(&self) -> bool {
|
||||||
|
self.active
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn activate(&mut self) {
|
||||||
|
self.active = true;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn oldest(&self) -> Option<Timestamp> {
|
pub fn oldest(&self) -> Option<Timestamp> {
|
||||||
self.oldest
|
self.oldest
|
||||||
}
|
}
|
||||||
@@ -496,16 +506,25 @@ impl Group {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn load_envelopes(&self, cx: &App) -> Task<Result<Vec<Event>>> {
|
pub(crate) fn load_envelopes(&self, cx: &App, state_only: bool) -> Task<Result<Vec<Event>>> {
|
||||||
let cache_tag = self.key.cache_tag();
|
let cache_tag = self.key.cache_tag();
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let filter = Filter::new()
|
let mut filter = Filter::new()
|
||||||
.kind(Kind::ApplicationSpecificData)
|
.kind(Kind::ApplicationSpecificData)
|
||||||
.custom_tag(SingleLetterTag::LOWERCASE_R, cache_tag);
|
.custom_tag(SingleLetterTag::LOWERCASE_R, cache_tag);
|
||||||
|
|
||||||
|
if state_only {
|
||||||
|
let kinds = STATE_KINDS
|
||||||
|
.iter()
|
||||||
|
.chain(MEMBERSHIP_KINDS.iter())
|
||||||
|
.map(|kind| kind.to_string());
|
||||||
|
|
||||||
|
filter = filter.custom_tags(SingleLetterTag::LOWERCASE_K, kinds);
|
||||||
|
}
|
||||||
|
|
||||||
let mut events: Vec<Event> = client
|
let mut events: Vec<Event> = client
|
||||||
.database()
|
.database()
|
||||||
.query(filter)
|
.query(filter)
|
||||||
|
|||||||
+77
-4
@@ -549,7 +549,7 @@ impl GroupsRegistry {
|
|||||||
);
|
);
|
||||||
|
|
||||||
self.subscribe(key.clone(), cx);
|
self.subscribe(key.clone(), cx);
|
||||||
self.reload(key, None, cx);
|
self.load_state(key, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscribe(&mut self, key: GroupKey, cx: &mut Context<Self>) {
|
fn subscribe(&mut self, key: GroupKey, cx: &mut Context<Self>) {
|
||||||
@@ -679,7 +679,7 @@ impl GroupsRegistry {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let logged = key.clone();
|
let logged = key.clone();
|
||||||
let load = group.read(cx).load_envelopes(cx);
|
let load = group.read(cx).load_envelopes(cx, false);
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |_this, cx| {
|
self.tasks.push(cx.spawn(async move |_this, cx| {
|
||||||
if let Some(event) = event
|
if let Some(event) = event
|
||||||
@@ -704,6 +704,71 @@ impl GroupsRegistry {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load only the group state (metadata, members, membership) from the cache.
|
||||||
|
fn load_state(&mut self, key: GroupKey, cx: &mut Context<Self>) {
|
||||||
|
let Some(group) = self.group(&key, cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(me) = NostrRegistry::global(cx).read(cx).current_user() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let logged = key.clone();
|
||||||
|
let load = group.read(cx).load_envelopes(cx, true);
|
||||||
|
|
||||||
|
self.tasks
|
||||||
|
.push(cx.spawn(async move |_this, cx| match load.await {
|
||||||
|
Ok(events) => {
|
||||||
|
group.update(cx, |group, cx| {
|
||||||
|
group.hydrate(events, me, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("nip29: loading {} state failed: {error}", logged.id());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cache and absorb a single event without rebuilding the group.
|
||||||
|
fn absorb(&mut self, key: GroupKey, event: Event, cx: &mut Context<Self>) {
|
||||||
|
let Some(group) = self.group(&key, cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(me) = NostrRegistry::global(cx).read(cx).current_user() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn(async move |_this, cx| {
|
||||||
|
if let Err(error) = group
|
||||||
|
.read_with(cx, |group, cx| group.save_envelope(event.clone(), cx))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
log::warn!("nip29: caching failed: {error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
group.update(cx, |group, cx| {
|
||||||
|
group.hydrate(vec![event], me, cx);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the full group content on first open.
|
||||||
|
pub fn activate(&mut self, key: &GroupKey, cx: &mut Context<Self>) {
|
||||||
|
let Some(group) = self.group(key, cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if group.read(cx).is_active() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
group.update(cx, |group, _cx| group.activate());
|
||||||
|
self.reload(key.clone(), None, cx);
|
||||||
|
}
|
||||||
|
|
||||||
fn forget_event(&mut self, group: Entity<Group>, event: Event, cx: &mut Context<Self>) {
|
fn forget_event(&mut self, group: Entity<Group>, event: Event, cx: &mut Context<Self>) {
|
||||||
let Some(target) = event.tags.event_ids().next() else {
|
let Some(target) = event.tags.event_ids().next() else {
|
||||||
return;
|
return;
|
||||||
@@ -761,7 +826,15 @@ impl GroupsRegistry {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.reload(key, Some(event), cx);
|
let active = self
|
||||||
|
.group(&key, cx)
|
||||||
|
.is_some_and(|group| group.read(cx).is_active());
|
||||||
|
|
||||||
|
if active {
|
||||||
|
self.reload(key, Some(event), cx);
|
||||||
|
} else {
|
||||||
|
self.absorb(key, event, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn absorb_candidate(&mut self, id: &SubscriptionId, event: &Event, cx: &mut Context<Self>) {
|
fn absorb_candidate(&mut self, id: &SubscriptionId, event: &Event, cx: &mut Context<Self>) {
|
||||||
@@ -819,7 +892,7 @@ impl GroupsRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if id == fork_subscription_id() {
|
if id == fork_subscription_id() {
|
||||||
log::warn!("nip29: the admins' group lists were refused: {reason}");
|
log::debug!("nip29: the admins' group lists were refused: {reason}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,13 @@ fn opens_run(rows: &[Row], index: usize) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(group: Entity<Group>, window: &mut Window, cx: &mut App) -> Entity<GroupPanel> {
|
pub fn init(group: Entity<Group>, window: &mut Window, cx: &mut App) -> Entity<GroupPanel> {
|
||||||
|
let key = group.read(cx).key().clone();
|
||||||
|
let groups = GroupsRegistry::global(cx);
|
||||||
|
|
||||||
|
groups.update(cx, |registry, cx| {
|
||||||
|
registry.activate(&key, cx);
|
||||||
|
});
|
||||||
|
|
||||||
cx.new(|cx| GroupPanel::new(group, window, cx))
|
cx.new(|cx| GroupPanel::new(group, window, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
Open items from the search and lazy-loading work. Each entry notes the known
|
||||||
|
tradeoff and where the fix would land.
|
||||||
|
|
||||||
|
## nip29
|
||||||
|
|
||||||
|
- [ ] **Incremental event handling for open groups.** Live events for an open
|
||||||
|
(active) group still trigger a full envelope reload and re-hydrate of the
|
||||||
|
whole group on every event (`GroupsRegistry::reload` in
|
||||||
|
`crates/nip29/src/lib.rs`). Closed groups already absorb events in O(1);
|
||||||
|
open groups could use the same incremental path, keeping the full reload
|
||||||
|
only as an occasional self-healing rebuild.
|
||||||
|
- [ ] **Fetch admins' group lists from NIP-29 relays.** The fork-detection
|
||||||
|
query (`GroupsRegistry::sync_forks`) is unscoped, so group relays refuse it
|
||||||
|
by policy (`must have 'h', 'e' or 'a' tag`). It still works via generic and
|
||||||
|
gossip relays, but the deeper fix is a per-relay scoped subscription using
|
||||||
|
an `a` tag with `10009:<admin-pk>` coordinates. The exact coordinate format
|
||||||
|
each relay accepts needs to be tested first.
|
||||||
Reference in New Issue
Block a user