feat: NIP-51 people lists (kind 30000) with unified discovery#333
Open
rabble wants to merge 51 commits into
Open
feat: NIP-51 people lists (kind 30000) with unified discovery#333rabble wants to merge 51 commits into
rabble wants to merge 51 commits into
Conversation
Deploying divine-web with
|
| Latest commit: |
7afb2d2
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://5b166c23.divine-web.pages.dev |
| Branch Preview URL: | https://feat-people-lists.divine-web.pages.dev |
🚀 Preview Deployment
|
Approved design spec for adding NIP-51 follow sets (people lists) alongside the existing video lists (kind 30005). Public-only in v1; unified discovery surfaces; preserves the existing video-list infra. Two prerequisite refactors are called out in the spec: - ListDetailPage hardcoded kinds query → support kind 30000 - ProfilePage gets a Tabs primitive (Videos | Lists) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step-by-step implementation plan against the approved spec. Eight chunks: foundation/refactors, read hooks, mutations, saved-lists, dialogs, detail surfaces, discovery surfaces, quick-add + verification. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop loops aggregate from v1 (bulk-users endpoint lacks total_loops); render '—' always; document deferral. - Pin actual fetchBulkUsers signature and isFunnelcakeAvailable(apiUrl) arg. - Correct SearchPage existing tabs (all|videos|users|hashtags); add lists as 5th tab with concrete grid-cols-5 + insertion line. - Split Task 6.5 (ListDetailPage refactor) into 5 sub-tasks 6.5a-e. - Add Task 4.1.5 useResolvedSavedLists for spec's stale-reference filter (sidebar Saved subgroup + /lists Saved tab). - Pin Task 3.4 cache invalidation to include single-list key. - Pin VideoCard menu location with grep first; concrete dropdown snippet. - Add brand-primitives reminder block (Card variant, SectionHeader, Button sticker) and responsive reminder (lg breakpoint, mobile dark). - Add canonical TDD step template (1-6) at top so condensed tasks expand consistently. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add buildListMembersPath, buildListVideosPath, buildListEditPath, and decodeListIdParam helpers to eventRouting.ts with 5 new TDD tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
React Query hook that fetches all kind-30000 events for a pubkey, dedupes by d-tag keeping latest created_at, and returns sorted PeopleList array. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
React Query hook keyed by ['people-list', pubkey, dTag] that fetches a single kind-30000 event by d-tag, picks the latest if multiple are returned, and parses via parsePeopleList (returns null when not found). Disabled when either pubkey or dTag is empty. Four tests covering the single-event, tie-breaking, empty-relay, and disabled cases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Composition hook that wires usePeopleList → useBatchedAuthors so
callers get a flat [{pubkey, metadata}] array plus combined loading/error
flags in a single import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces usePeopleListMemberVideos(pubkey, dTag) — an infinite-scroll hook that aggregates videos from all members of a NIP-51 people list. Primary path queries the relay (kinds:[34236], authors: members) with cursor-based `until` pagination; hard-caps total events at 500. The REST path (POST /api/videos/bulk with from_event kind 30000) is stubbed as a TODO comment pending Funnelcake support. 4 vitest tests cover the empty-members gate, single-page sort order, two-page pagination, and the loading-state gate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements useCreatePeopleList mutation: generates a UUID d-tag, publishes kind 30000 with title/description/image/p tags (members deduped), and optimistically prepends the new PeopleList to the query cache before invalidating. Covered by 4 vitest tests (no-user throw, tag assertions, optional-tag omission, cache prepend). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements mutation to update name, description, and image on an existing kind 30000 people list. Reads current event from relay, rebuilds tags preserving all p-tags, and republishes. Empty string for description/image clears the tag; undefined preserves existing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…timistic updates Both mutations are idempotent, read the current relay event before publishing, and apply optimistic cache updates with snapshot-based rollback on error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements delete for kind 30000 people lists via NIP-09 deletion events. Mirrors useDeleteVideoList pattern: publishes kind 5 with both 'a' and 'k' tags for NIP-09 conformance, optimistically removes from cache, then invalidates both ['people-lists', pubkey] and ['people-list', pubkey, listId] caches. Tests: throws when no user, publishes kind-5 with both tags, cache removal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reads the user's kind 30003 'saved-lists' addressable event and parses a tags into typed SavedListRef objects (kind 30000 | 30005). Drops malformed refs (wrong kind, non-hex pubkey, missing colons). Disabled when no current user; staleTime 60s. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exports parseVideoList from useVideoLists.ts (was private) so the new resolver can call it. useResolvedSavedLists composes useSavedLists() + useQueries to fetch each saved ref; refs where the relay returns 0 events or an unexpected kind (e.g. a deletion tombstone) are silently dropped. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements optimistic-update mutations for saving/unsaving addressable list refs (kind 30000/30005) in the user's kind 30003 saved-lists event. Both hooks are idempotent, validate input, and rollback the ['saved-lists', pubkey] cache on publish failure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Composition hook over usePeopleLists and useVideoLists. Returns both lists with unified loading/error states. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dialog for creating NIP-51 kind 30000 people lists with name, description, and image URL fields. Validates required name and URL format, shows prefilled-members count when provided, and calls useCreatePeopleList on submit with proper error/success toasts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors CreatePeopleListDialog for editing existing NIP-51 people lists. Pre-populates name/description/image on open, calls useUpdatePeopleList, shows "Saved. Looking sharp." on success, and closes the dialog. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Confirmation dialog for deleting people lists. Mirrors DeleteListDialog pattern with destructive variant button, warning text, and mutation integration via useDeletePeopleList hook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Checkbox rows per list let users add/remove a person from any of their people lists in a single click; empty state shows create CTA only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the public list-detail page header with back nav, sticker action buttons (Follow/Following/Edit list), SectionHeader title, stats row (members·videos·loops, — for null), description, and avatar strip with View all link. 5 vitest tests, tsc clean, brand guardrails pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Vertical member-row grid for people list detail pages (Figma #6): avatar + Bricolage ExtraBold display_name + Inter sub-line (NIP-05 or truncated npub), with per-row MinusCircle (editMode+owner) or DotsThree overflow action. 5 vitest tests all pass; tsc clean; brand guardrails pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wraps usePeopleListMemberVideos + parseVideoEvents into a VideoGrid with useContentModeration isMuted filtering on author pubkeys. 4 vitest tests cover render count, empty state, loading passthrough, and muted-author exclusion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the owner curate screen (Figma #8) with top bar navigation, members grid in edit mode, and user search/add candidates section. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move kind-30005 video grid rendering out of ListDetailPage into a new VideoListContent component. ListDetailPage is now thin: it fetches the event, parses with parseVideoList, and delegates rendering to VideoListContent. No behavior change for kind-30005; kind-30000 body is left empty pending Task 6.5b. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the relay returns a kind-30000 event, ListDetailPage now renders PeopleListContent (header + videos-from-members grid) instead of the kind-30005 VideoListContent path. Raw event is retained in query result so kind dispatch happens at render time. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds ListMembersPage at /list/:pubkey/:listId/members, wiring usePeopleList + PeopleListMembersGrid with a top bar (back arrow, list name via SectionHeader, member count, owner add-button) and loading/not-found states. Route registered in AppRouter under the public group. Three vitest tests cover header, grid, and loading. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds ListVideosPage at /list/:pubkey/:listId/videos, wiring usePeopleList + PeopleListVideosGrid with a top bar (back arrow, list name via SectionHeader) and loading/not-found states. Route registered in AppRouter under the public group alongside the members sub-route. Two vitest tests cover header+grid and loading state. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Wire up ListEditPage under the authenticated route group; non-owners and logged-out visitors are immediately redirected to the detail page. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-column mosaic media area (177:120 ratio) with avatar tiles, member count scrim badge, SectionHeader title and optional description. Falls back to pubkey-hashed colour swatches when no membersPreview is given. Links to buildListPath. All 4 vitest tests pass; brand guardrails clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces VideoListCard (kind 30005 discovery card — thumbnail tile, play-badge, title, description) and UnifiedListCard which dispatches to PeopleListCard or VideoListCard based on the Nostr kind discriminant. All 5 unit tests pass; brand and tsc clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces a Videos | Lists Tabs primitive to ProfilePage for the first time. URL hash sync: #lists deep-links to the Lists tab on load; tab changes update the hash via replace navigation. ProfileListsTab renders a 2-col (mobile) / 4-col (desktop) grid of UnifiedListCard entries, sorted by recency across both people (kind 30000) and video (kind 30005) lists, with a sticker-variant create button gated to isOwn. Six new tests cover the component and page-level tab behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a new Lists tab to the Discovery page backed by useDiscoveryLists hook (kinds 30000+30005, ranked by member/video count × 10 + recency). Renders UnifiedListCard in a 2-col mobile / 4-col desktop grid. Includes i18n keys for all 15 locales and full vitest coverage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a 5th tab (Lists) to the SearchPage that queries kind 30000 (people lists) and 30005 (video sets) via NIP-50 relay search with a local-filter fallback. Introduces useSearchLists hook and renders results through UnifiedListCard in a 2-col/4-col grid. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a login-gated Lists section to AppSidebar showing authored lists (video+people, capped at 8) and saved lists subgroup from useUnifiedLists/useResolvedSavedLists, with a + Create new list CTA that opens CreatePeopleListDialog. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace video-only ListsPage with two sub-tabs (Authored / Saved) that display both PeopleLists and VideoLists via useUnifiedLists and useResolvedSavedLists. Lists are flattened and sorted by createdAt desc, rendered as a 2-col mobile / 4-col desktop UnifiedListCard grid. Authored empty state shows the Create CTA; Saved empty state shows a subdued message only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an 'Add to list...' DropdownMenuItem (UsersThree icon) to the other-user profile overflow menu, wired to AddToPeopleListDialog. Hidden on own profile. Covered by 3 new TDD tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a context-menu item that opens AddToPeopleListDialog when a logged-in user views a video by someone else, allowing one-tap addition to any people list. Hidden when the video belongs to the current user. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each row in UserListDialog gets a DotsThree button that opens AddToPeopleListDialog to add the person to any of the user's people lists. Overflow is hidden when row pubkey matches current user. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI fails caught by `tsc -p tsconfig.app.json --noEmit` (stricter than the root tsconfig used locally): - SectionHeader: extend `as` prop to accept "h1" (used by 4 callsites in detail header, edit mode, members/videos pages). - PeopleListCard: AvatarTile/PlaceholderSwatch now accept a `style` prop so the mosaic can size tiles via inline styles. - ProfileListsTab: narrow the discriminated union before passing to UnifiedListCard (kind 30000 vs 30005 branches). - Test mocks: cast through `unknown` to satisfy UseQueryResult / UseMutationResult shape checks for the QueryClient v5 type. - PeopleListMembersGrid.test: widen `MemberOverride` type to allow display_name + name overrides in test fixtures. Build: bump workbox `maximumFileSizeToCacheInBytes` from 3MB to 4MB. The new people-lists code pushed the main bundle from 2.95 MB to 3.15 MB, exceeding the prior limit. Code-splitting is the right long-term fix; bumping the limit unblocks this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The project's eslint config sets `noInlineConfig: true` so disable directives don't work. Replaced all `as any` mock casts with proper `as unknown as ReturnType<typeof useFoo>` chains, and removed unused imports (waitFor, fireEvent, useNavigate, PassThrough, SearchListResult, MEMBERS_PREVIEW). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Other Nostr clients abuse kind 30000 for mute lists, block lists, and dm-contact lists with reserved d-tags like "Block List", "mute", and "dm-contacts". These were polluting /discovery/lists, search, and even user profile list tabs as if they were curated follow sets. - parsePeopleList rejects reserved d-tags (mute/block/dm-contacts/etc.) case-insensitively — applies everywhere people lists surface. - useDiscoveryLists and useSearchLists additionally require a `title` tag, so events whose name would fall back to the d-tag don't appear on public discovery surfaces. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VideoListCard hardcoded /list/videos/<pubkey>/<id>, but the route is registered as /list/:pubkey/:listId/videos (videos as a SUFFIX, not a prefix), so clicking any video-list discovery card hit the 404 page. Use buildListPath like PeopleListCard does — the kind-30005 detail view already lives at /list/<pubkey>/<id> via ListDetailPage's kind dispatch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empty people lists and empty video lists were rendering as cards with 0-count badges and placeholder swatches — nothing to thumbnail and nothing to engage with. Filter both surfaces to require at least one member (kind 30000) or one video coordinate (kind 30005). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Discovery > Lists previously rendered placeholder swatches because no member profiles or video thumbnails were ever fetched. Now: - New useDiscoveryListPreviews hook batch-fetches profiles for the first 3 members of each people list (warming the useAuthor cache) and queries kind 34236 for the first video coordinate of each video list, extracting its imeta thumbnail. - UnifiedListCard threads the previews helper into PeopleListCard's membersPreview prop and VideoListCard's new thumbnail prop. - VideoListCard prefers the fetched thumbnail and falls back to list.image (the optional cover tag) before showing a placeholder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The since: now - 7 days filter was a self-inflicted bug — addressable events (kinds 30000/30005) republish only when edited, so a 7-day window excluded virtually every real curated list. Pull a wide pool (limit 500), dedupe by pubkey:kind:dTag keeping the newest, then rank locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds support for NIP-51 follow sets (kind 30000) — public people lists — alongside the existing video lists (kind 30005). Users can create, edit, delete people lists; add/remove people from any user-bearing surface; subscribe to others' lists via NIP-51 bookmark sets (kind 30003); and discover both kinds together via a unified Lists experience.
Mobile designs from Figma (
UI-Design, file `rp1DsDEUuCaicW0lk6I2aZ`) — adapted via the existing< lgmobile /≥ lgdesktop responsive split.Spec:
docs/superpowers/specs/2026-05-05-people-lists-design.mdPlan:
docs/superpowers/plans/2026-05-05-people-lists.mdSurfaces
/discovery, Lists tab on/search, Lists tab on every Profile page (Videos | Lists), Lists section inAppSidebar(authored + saved)./list/:pubkey/:listIdauto-routes to people-list vs video-list rendering. Sub-routes:/members,/videos, owner-guarded/edit.CreatePeopleListDialog,EditPeopleListDialog,DeletePeopleListDialog,AddToPeopleListDialog.ProfileHeader,VideoCard,UserListDialogrows.Backend / data layer
src/hooks/usePeople*andsrc/hooks/useSaved*.fetchBulkUsersREST endpoint, capped at 200 members; loops aggregate is deferred (bulk endpoint shape lackstotal_loops— documented in spec).Drive-by fixes called out in the spec review
useDeleteVideoListwas missing['k', '30005']on its NIP-09 kind 5 deletion event — fixed for NIP-09 conformance (src/hooks/useVideoLists.ts:530).LIST_KINDSinNostrProvider.tsxextended from[30000, 30001, 30005]to[30000, 30001, 30003, 30005]so the new saved-lists kind also goes to multi-relay.ListDetailPagequery widened fromkinds: [30005]tokinds: [30000, 30005]and split into a dispatcher.ProfilePagenow has aTabsprimitive (none before this PR).parseVideoListexported fromuseVideoLists.tssouseResolvedSavedListscan reuse it.Brand & a11y
<SectionHeader>,<Card variant="brand">,<Button variant="sticker">.@phosphor-icons/reactonly.uppercaseclass, nolucide-react, no gradients).Explicit deferrals (in spec, out of v1)
content)—always)Test plan
/lists/lists/<your-pubkey>/<list-id>and confirm Header (stats / description / avatar strip) + recent-videos grid render/membersroute renders the full roster/discoveryLists tab; verify both kinds appear with correct count badges (▶ vs 👥)Branch / commits
44 commits, mostly small TDD steps. The plan was executed via subagent-driven-development; each task includes failing-test → minimal-impl → tests-pass → commit. 874 tests passing across 166 files post-rebase.
🤖 Generated with Claude Code