Files
Artstr-Studio/docs/COLLECTIONS_FEATURE.md
T
2026-05-15 21:13:49 -04:00

7.2 KiB
Raw Blame History

Collections Feature Plan (NIP-51)

Overview

Let users organize their favorite community templates into named collections — "My Pixar Set", "Horror Night", "Criterion Shelf" — and share them publicly via Nostr. Anyone can browse a collection and import templates from it directly. Collections are standard NIP-51 bookmark sets (kind 30001), so they sync across Nostr clients automatically.

Complexity

Low–Moderate — Estimated 3–5 hours


Nostr Primitive: NIP-51 Bookmark Sets (kind 30001)

Kind 30001 is an addressable (replaceable) event that holds a named list of references. For template collections, each list item is an a tag pointing to a kind 30078 template:

{
  "kind": 30001,
  "pubkey": "<user-pubkey>",
  "tags": [
    ["d", "my-pixar-set"],
    ["title", "My Pixar Set"],
    ["description", "All my Pixar case wraps"],
    ["image", "https://...cover.jpg"],
    ["a", "30078:<creator-pubkey>:<d-tag>", "<relay-hint>"],
    ["a", "30078:<creator-pubkey>:<d-tag>", "<relay-hint>"]
  ],
  "content": ""
}
  • The d tag is the collection's unique identifier (slug). Updating with the same d replaces the previous version — no duplicate events accumulate.
  • title, description, and image are optional display metadata.
  • Each a tag references one template (kind 30078 addressable event).

Key Components

1. Creating a Collection

async function createCollection(name, description) {
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
  const event = {
    kind: 30001,
    tags: [
      ['d', slug],
      ['title', name],
      ['description', description],
    ],
    content: '',
    created_at: Math.floor(Date.now() / 1000),
  };
  await publishToRelays(await window.nostr.signEvent(event));
  return slug;
}

2. Adding/Removing a Template from a Collection

Since kind 30001 is a replaceable event, adding an item means fetching the current version, appending the new a tag, and re-publishing:

async function addToCollection(collectionSlug, templateEvent) {
  const aTag = buildATag(templateEvent);        // '30078:<pubkey>:<d-tag>'
  const current = await fetchMyCollection(collectionSlug);
  const tags = current ? [...current.tags] : [['d', collectionSlug]];
  if (!tags.some(t => t[0] === 'a' && t[1] === aTag)) {
    tags.push(['a', aTag, '']);
  }
  await publishToRelays(await window.nostr.signEvent({
    kind: 30001, tags, content: '', created_at: Math.floor(Date.now() / 1000),
  }));
}

async function removeFromCollection(collectionSlug, templateEvent) {
  const aTag = buildATag(templateEvent);
  const current = await fetchMyCollection(collectionSlug);
  if (!current) return;
  const tags = current.tags.filter(t => !(t[0] === 'a' && t[1] === aTag));
  await publishToRelays(await window.nostr.signEvent({
    kind: 30001, tags, content: '', created_at: Math.floor(Date.now() / 1000),
  }));
}

3. Fetching Collections

async function fetchMyCollections() {
  // Query kind 30001 authored by myPubkey
  return await queryRelays(relays, {
    kinds: [30001],
    authors: [state.community.myPubkey],
  }, { timeoutMs: 3000 });
}

async function fetchCollectionByAddress(pubkey, slug) {
  // Fetch a specific collection by pubkey + d-tag for sharing/browsing
  return await queryRelays(relays, {
    kinds: [30001],
    authors: [pubkey],
    '#d': [slug],
  }, { timeoutMs: 3000 });
}

4. Browsing a Shared Collection

Collections are shareable via a URL fragment or Nostr address (naddr):

casewrap.app/#collection/naddr1...

When a collection address is opened:

  1. Fetch the kind 30001 event by naddr
  2. Extract all a tags → list of template addresses
  3. Fetch each kind 30078 template event
  4. Render them as a mini-feed with "Import All" option

5. State Additions

// Add to state:
collections: {
  mine: [],              // user's own kind 30001 events
  viewing: null,         // currently browsed collection (own or external)
}

UI

Template Card — "Save to Collection" Menu Item

Add to the card action menu (⋮):

⑃ Remix this design
📁 Save to collection  ▶  [My Pixar Set]
                          [Horror Night]
                          [+ New collection…]

Collections Panel (in sidebar or settings)

┌─────────────────────────────────────────┐
│  📁 My Collections            [+ New]   │
│                                          │
│  My Pixar Set          8 templates  [→]  │
│  Horror Night          3 templates  [→]  │
│  Criterion Shelf      12 templates  [→]  │
└─────────────────────────────────────────┘

Collection Detail View

Clicking a collection opens a mini-feed inside the community panel:

← Back    My Pixar Set  (8 templates)
─────────────────────────────────────
[Import All]  [Share]  [Edit]  [Delete]

[Card] [Card] [Card] ...

"Share" copies the naddr Nostr address to the clipboard for sharing.

Feed Cards — Collection Badge

If a template is in one of the user's collections, show a small 📁 icon on the card corner.


Performance Considerations

  • Kind 30001 events are small (just tags) — fast to fetch and publish
  • Fetch the user's collections once at login, cache in state.collections.mine
  • Re-fetch lazily when the collections panel is opened
  • Template events inside a collection are fetched only when the collection detail is opened

Limitations

  • Requires NIP-07 login to create or modify collections
  • Read-only browsing of other users' shared collections works without login
  • Nostr clients that don't parse kind 30001 a tags (only e tags) may not display templates correctly — this is a client compatibility issue, not a bug here
  • No private/encrypted collections — kind 30001 is a public event (NIP-51 has a draft for private lists but it is not widely supported)

Future Enhancements

  • Collaborative collections: multiple pubkeys can contribute (needs a separate coordination event)
  • Collection cover image: auto-generated from the first template's preview image
  • "Import All" bulk-imports every template in a collection into a local queue
  • Featured collections: curated by app admins on a discovery page
  • RSS/Atom feed for a collection (static export)

Testing Checklist

  • Creating a collection publishes a valid kind 30001 event with d and title tags
  • Adding a template adds the correct a tag and re-publishes the event
  • Removing a template removes the a tag without affecting others
  • Duplicate adds are ignored (same a tag not added twice)
  • Collections panel lists all user's kind 30001 events with correct counts
  • Collection detail fetches and renders referenced template events
  • "Share" copies a valid naddr to clipboard
  • Shared collection URL loads and renders without login
  • "Import All" imports each template in the collection
  • 📁 badge appears on feed cards that are in a user collection
  • Collections sync correctly after page reload (re-fetched from relays)