People tab rewrite: recency sort, profile-post Discover, bio modal,

per-author feed filter, ignore primitive

The old People tab was built on network-layer presence (`is_online`,
`last_seen` from the mesh), which was lost when v0.6.1 anonymized the
network id from the posting id. Every named follow is authored under a
posting id that doesn't appear in the connection-layer tables; the
"Online" section listed nobody useful and Discover depended on the
same broken signal.

Replaced with signals derived from signed content:
- Following is sorted by most-recent-post timestamp (the real meaning
  of "activity" in a post-anonymization world).
- Discover lists named peers we've received signed profile posts from
  (via Phase 2d), filtered by follows / ignores / self.
- Click-a-name surfaces a bio modal with View Posts / Follow / Message
  / Ignore actions.
- Author-scoped feed filter (`View Posts` on any person) renders a
  "Showing posts from X" banner with a Clear button.
- Ignore is a new local-only primitive; ignored peers' posts and
  profiles are excluded everywhere and the ignored list is editable
  in Settings.

Core changes:

- New `ignored_peers(node_id, ignored_at)` table + storage helpers
  (`add_ignored_peer`, `remove_ignored_peer`, `list_ignored_peers`,
  `is_ignored_peer`). Schema created fresh; no migration since the
  table is purely additive and empty on prior installs.
- All 6 feed-query sites now also exclude `author IN ignored_peers`.
- New `Storage::last_activity_for_authors(&[NodeId])` — one batched
  query returning max post timestamp per author, excluding non-feed
  intents (Control / Profile / Announcement / GroupKeyDistribute).
- New `Storage::list_discoverable_profiles(&self_id)` — named profile
  rows where node_id is not self, not in follows, not in ignored, and
  `public_visible = 1`. Sorted by profile `updated_at` DESC.
- New `Storage::delete_setting(key)` — missing counterpart to
  set/get.
- Node wrappers: `last_activity_for_follows`, `ignore_peer`
  (also drops any follow + social route for the ignored peer),
  `unignore_peer`, `list_ignored_peers`, `list_discoverable_profiles`.
- `list_follows` Tauri command now sources `last_activity_ms` from
  the posts-driven batched query rather than the network peer record.

Tauri commands: `list_discover`, `ignore_peer`, `unignore_peer`,
`list_ignored_peers`.

Frontend:
- Following list: see-new-activity button pattern (staged data +
  explicit user click to rearrange, so the list doesn't reorder under
  a tap mid-scroll). Periodic people-tab polling stages + lights up
  the button; clicking it re-renders.
- Discover: rewrites the old peer-table-based list to a profile-post
  feed. Each card shows name + bio + profile-update age, plus Follow
  / Posts / Ignore actions.
- Bio modal: reuses the existing generic popover. Loads display name
  + bio via `resolve_display`, shows follow state, offers View Posts /
  Follow-or-Unfollow / Message / Ignore-or-Unignore.
- Author filter: banner renders at the top of the feed when active;
  clear button restores full feed. Filter state is a single `authorFilterNodeId`
  field consumed by `filterFeedPosts`.
- Settings → Ignored section lists ignored peers with unignore buttons.

124 / 124 core tests pass.
This commit is contained in:
Scott Reimers 2026-04-23 12:15:51 -04:00
parent e74bd4e6c6
commit 2ce668aa58
5 changed files with 595 additions and 168 deletions

View file

@ -1444,6 +1444,47 @@ impl Node {
storage.list_follows()
}
/// Batch: for each followed author, return the last-post timestamp we
/// hold locally. Used by the Following UI to sort by recency (which
/// replaces the broken "online" indicator since the network/posting
/// key split anonymized presence).
pub async fn last_activity_for_follows(&self) -> anyhow::Result<std::collections::HashMap<NodeId, u64>> {
let storage = self.storage.get().await;
let follows = storage.list_follows()?;
storage.last_activity_for_authors(&follows)
}
// ---- Ignored peers ----
pub async fn ignore_peer(&self, node_id: &NodeId) -> anyhow::Result<()> {
let storage = self.storage.get().await;
storage.add_ignored_peer(node_id)?;
// If the peer was in follows, also drop them — ignoring implies
// no-longer-following. Best-effort; errors are logged by callers.
let _ = storage.remove_follow(node_id);
let _ = storage.remove_social_route(node_id);
Ok(())
}
pub async fn unignore_peer(&self, node_id: &NodeId) -> anyhow::Result<()> {
let storage = self.storage.get().await;
storage.remove_ignored_peer(node_id)
}
pub async fn list_ignored_peers(&self) -> anyhow::Result<Vec<NodeId>> {
let storage = self.storage.get().await;
storage.list_ignored_peers()
}
// ---- Discover ----
/// Named peers we aren't following and haven't ignored — driven entirely
/// by signed profile posts we've received through the CDN.
pub async fn list_discoverable_profiles(&self) -> anyhow::Result<Vec<PublicProfile>> {
let storage = self.storage.get().await;
storage.list_discoverable_profiles(&self.default_posting_id)
}
// ---- Profiles ----
/// Set the default posting identity's profile (display_name, bio,