Split nostr-kinds skill into design and rendering
The combined skill conflated two unrelated jobs: (1) design-time
decisions when authoring a new kind (NIP-vs-custom, ranges, tag design,
NIP.md) and (2) implementation-time checklist for wiring rendering into
Ditto's many UI touchpoints. The single description sentence was
unwieldy, and its trigger ('introducing a new kind... or registering a
kind in the UI') was phrased around the author's perspective — it
didn't match user phrasing like 'support displaying kind X' or 'render
NIP-Y', so I skipped loading it when implementing NIP-84 and missed
half the registration points.
Split into:
- nostr-kind-design — NIP-vs-custom decision, kind ranges, tag design,
content-vs-tags, NIP.md. Loads when minting or extending a schema.
- nostr-kind-rendering — the multi-location UI registration checklist.
Loads when rendering a kind Ditto doesn't yet display, or when asked
to 'support / display / render' a NIP or kind number.
Expanded the rendering checklist with the points I missed during the
NIP-84 pass: the six-file notification stack, the four-file AppConfig
triple for feed-toggle keys, sidebar icon registration, AppRouter route
wiring, shouldHideFeedEvent spam guards, and a 'bugs that signal a
missed step' section so the checklist reads as diagnostic too. Also
flagged the embedded-previews trap where skipping the dispatcher branch
silently feeds quoted prose through the kind-1 tokenizer.
Updated both AGENTS.md references to point at the two new skills.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: nostr-kind-design
|
||||
description: Decide whether to reuse an existing NIP or mint a new kind, design tag structures that relays can index, choose what goes in content vs. tags, and document new kinds or extensions in NIP.md. Load when authoring a new schema — not when wiring up rendering for a kind that already exists (use nostr-kind-rendering for that).
|
||||
---
|
||||
|
||||
# Nostr Kinds — Design and Schema
|
||||
|
||||
Load this skill when:
|
||||
|
||||
- Minting a new event kind for a Ditto feature.
|
||||
- Extending an existing NIP with new tags.
|
||||
- Deciding whether an existing NIP covers a use case or whether a custom kind is warranted.
|
||||
- Documenting a custom kind or extension in `NIP.md`.
|
||||
|
||||
**Not this skill** — if an existing NIP/kind covers your use case and you only need to render it in Ditto's UI, use the **`nostr-kind-rendering`** skill instead.
|
||||
|
||||
## Choosing Between Existing NIPs and Custom Kinds
|
||||
|
||||
1. **Thorough NIP review first.** Browse the NIP index, then read candidate NIPs in detail. The goal is to find the closest existing solution.
|
||||
2. **Prefer extending existing NIPs** over creating custom kinds, even at the cost of minor schema compromises. Custom kinds fragment the ecosystem.
|
||||
3. **When an existing NIP is close but not perfect**, use its kind as the base and add domain-specific tags. Document the extension in `NIP.md`.
|
||||
4. **Only mint a new kind** when no existing NIP covers the core functionality, the data structure is fundamentally different, or the use case requires different storage characteristics (regular vs. replaceable vs. addressable).
|
||||
5. **If a tool to generate a new kind number is available, you MUST call it.** Never pick an arbitrary number.
|
||||
6. **Custom kinds MUST include a NIP-31 `alt` tag** with a human-readable description of the event's purpose.
|
||||
|
||||
**Example decision:**
|
||||
|
||||
```
|
||||
Need: Equipment marketplace for farmers
|
||||
Options:
|
||||
1. NIP-15 (Marketplace) — too structured for peer-to-peer sales
|
||||
2. NIP-99 (Classifieds) — good fit, extensible with farming tags
|
||||
3. Custom kind — perfect fit, no interoperability
|
||||
|
||||
Decision: NIP-99 + farming-specific tags.
|
||||
```
|
||||
|
||||
## Kind Ranges
|
||||
|
||||
An event's kind number determines storage semantics:
|
||||
|
||||
- **Regular** (1000 ≤ kind < 10000) — stored permanently by relays. Notes, articles, etc.
|
||||
- **Replaceable** (10000 ≤ kind < 20000) — only the latest event per `pubkey+kind` is kept. Profile metadata, contact lists, mute lists.
|
||||
- **Addressable** (30000 ≤ kind < 40000) — identified by `pubkey+kind+d-tag`; only the latest per combo is kept. Long-form content, products, definitions.
|
||||
|
||||
Kinds below 1000 are "legacy"; storage is per-kind (e.g. kind 1 is regular, kind 3 is replaceable).
|
||||
|
||||
## Tag Design Principles
|
||||
|
||||
- **Kind = schema, tags = semantics.** Don't mint a new kind just to represent a different category of the same data.
|
||||
- **Relays only index single-letter tags.** Use `t` for categories so filters like `'#t': ['electronics']` work at the relay level. Multi-letter tags (`product_type`, etc.) force inefficient client-side filtering.
|
||||
- **Filter at the relay**, not in JavaScript:
|
||||
|
||||
```ts
|
||||
// ❌ Fetch everything, filter locally
|
||||
const events = await nostr.query([{ kinds: [30402] }]);
|
||||
const filtered = events.filter((e) => hasTag(e, 'product_type', 'electronics'));
|
||||
|
||||
// ✅ Filter at the relay
|
||||
const events = await nostr.query([{ kinds: [30402], '#t': ['electronics'] }]);
|
||||
```
|
||||
|
||||
- **For Ditto-specific niches** (community apps, regional variants), tag events with a `t` value and query on it. Don't do this for generic platforms — it would silo content.
|
||||
|
||||
## Content vs. Tags
|
||||
|
||||
- **`content`** — large freeform text or existing industry-standard JSON (GeoJSON, FHIR, Tiled maps). Kind 0 is the one exception where structured JSON goes in content.
|
||||
- **Tags** — queryable metadata, structured data, anything you might filter on.
|
||||
- **Empty content is fine.** `content: ""` is idiomatic for tag-only events.
|
||||
- **If you need to filter by a field, it must be a tag** — relays don't index content.
|
||||
|
||||
```json
|
||||
// ✅ Queryable
|
||||
{ "kind": 30402, "content": "",
|
||||
"tags": [["d", "product-123"], ["title", "Camera"], ["price", "250"], ["t", "photography"]] }
|
||||
|
||||
// ❌ Structured data buried in content
|
||||
{ "kind": 30402, "content": "{\"title\":\"Camera\",\"price\":250}", "tags": [["d", "product-123"]] }
|
||||
```
|
||||
|
||||
## `NIP.md`
|
||||
|
||||
`NIP.md` documents Ditto's custom kinds and any extensions to existing NIPs. Whenever you mint a new kind or change a custom schema, **create or update `NIP.md`** with the tag list, content format, and intended usage. If a kind you add is effectively the same shape as an existing NIP, note the NIP reference rather than duplicating the spec.
|
||||
|
||||
Standard NIPs (like NIP-84 Highlights, NIP-23 Articles) do **not** go in `NIP.md` — only Ditto-custom kinds and Ditto-specific extensions.
|
||||
|
||||
## After Designing — What's Next?
|
||||
|
||||
Once you've settled on a kind number and tag shape, you still need to render it in Ditto's UI. Load the **`nostr-kind-rendering`** skill for the full multi-location registration checklist (feed cards, detail pages, embedded previews, kind-label maps, notifications, feed-toggle registration).
|
||||
@@ -0,0 +1,185 @@
|
||||
---
|
||||
name: nostr-kind-rendering
|
||||
description: Add UI rendering for an event kind Ditto doesn't yet display — feed cards, detail pages, embedded previews, notifications, routes, feed-toggle registration, and the several kind-label maps (KIND_LABELS, KIND_HEADER_MAP, NOTIFICATION_KIND_NOUNS, CommentContext) that must stay in sync. Load when asked to "support / display / render" a NIP or kind number, when a kind renders blank or as "Kind 12345", or when quote embeds of a kind show "This event kind is not supported".
|
||||
---
|
||||
|
||||
# Nostr Kinds — UI Rendering Checklist
|
||||
|
||||
Ditto's kind dispatch is **spread across many files** by design — feed cards, detail pages, embedded previews, notifications, and several kind-label maps each have their own rendering requirements. The central `KIND_LABELS` registry covers the easy cases, but most context-specific maps (grammar, icons, verbs) cannot be derived mechanically and must be updated manually.
|
||||
|
||||
**Missing any location causes visible bugs**: a kind might render blank in quote posts, show "Kind 12345" as a label, skip its action header, tombstone as "This event kind is not supported" in embeds, or — worst of all — have its content fed through the kind-1 tokenizer and auto-linkify URLs/hashtags that weren't authored by the event creator.
|
||||
|
||||
**When in doubt, grep for an existing kind number like `30617` or `9802`** — you'll find every registration point you need to mirror.
|
||||
|
||||
## Decision: Feed-toggle + dedicated page, or just rendering?
|
||||
|
||||
Before touching code, pick one:
|
||||
|
||||
- **Just render it everywhere Nostr content appears** (no feed toggle, no dedicated page). Use when the kind is niche or only reached via direct links / quote embeds. Minimal surface — steps 1–6 below.
|
||||
- **Add a feed toggle + optional dedicated page.** Use when users should be able to browse events of this kind or opt them in/out of their home feed. Requires the feed registration (step 7) and `AppConfig` triple (step 8).
|
||||
|
||||
When the user asks generally to "support" a kind, ask which direction they want if it's not obvious from context.
|
||||
|
||||
## Checklist
|
||||
|
||||
### 1. Content card component (`src/components/`)
|
||||
|
||||
Create `<MyKindCard event={event} />` that renders the event's tags/content appropriately.
|
||||
|
||||
- **Never run event content through the kind-1 tokenizer** (`<NoteContent>` / `<TruncatedNoteContent>`) unless the kind's content is actually free-form user prose. Quote-type content (highlights, snippets, citations) contains URLs and hashtags from the *source*, not the event author — tokenizing them is misleading.
|
||||
- Render plaintext with `whitespace-pre-wrap break-words` inside a `<p>` instead.
|
||||
- Route any event-sourced URLs (`r` tags, media URLs, source links) through `sanitizeUrl()` from `@/lib/sanitizeUrl` before using them in `href`/`src`.
|
||||
- Support an `expanded` prop if the card looks different on the detail page than in the feed.
|
||||
|
||||
### 2. Feed card dispatch (`src/components/NoteCard.tsx`)
|
||||
|
||||
Three edits in this file:
|
||||
|
||||
1. **Flag block** (around lines 384–435): add `const isMyKind = event.kind === XXXX;`.
|
||||
2. **`isTextNote` negation list** (around lines 440–475): add `&& !isMyKind`. Without this, unknown kinds fall through to `UnknownKindContent` (showing only the `alt` tag).
|
||||
3. **Content dispatch ternary** (around lines 578–692): add `) : isMyKind ? (<MyKindCard event={event} />`.
|
||||
4. **`KIND_HEADER_MAP`** (around lines 1710+): add an entry so the feed shows "Alice shared a *noun*" or similar. Pattern:
|
||||
```ts
|
||||
9802: {
|
||||
icon: Highlighter,
|
||||
action: "shared a",
|
||||
noun: "highlight",
|
||||
nounRoute: "/highlights", // omit if no dedicated page
|
||||
},
|
||||
```
|
||||
5. Import the card component and any new lucide icons.
|
||||
|
||||
### 3. Detail page dispatch (`src/pages/PostDetailPage.tsx`)
|
||||
|
||||
Mirror the three NoteCard edits:
|
||||
|
||||
1. **Flag block** (around lines 1021–1098): `const isMyKind = event.kind === XXXX;`.
|
||||
2. **`isTextNote` negation list**: add `&& !isMyKind`.
|
||||
3. **Content dispatch ternary** (around lines 2147–2251): add `) : isMyKind ? (<MyKindCard event={event} expanded />`.
|
||||
|
||||
The loading-state title uses `shellTitleForKind()`, which falls through to `KIND_LABELS` — no override needed unless the kind belongs to a group ("Music Details") or needs composite grammar.
|
||||
|
||||
### 4. Central kind label (`src/lib/kindLabels.ts`)
|
||||
|
||||
Add a **capitalized noun phrase, no articles** to the `KIND_LABELS` map:
|
||||
|
||||
```ts
|
||||
9802: 'Highlight',
|
||||
```
|
||||
|
||||
This is consumed by the detail-page loading title, nsite permission prompt, signer nudge toasts, and addressable-event preview headers. Ignoring this gives "Kind 9802" everywhere it appears.
|
||||
|
||||
### 5. Context-specific label and icon maps
|
||||
|
||||
Each of these maps exists because the surrounding UI needs a different grammatical form. They are **not derived** from `KIND_LABELS` and must be updated manually.
|
||||
|
||||
- **`src/components/CommentContext.tsx`** — `KIND_LABELS` (uses articles: `'a highlight'`, `'an article'`) and `KIND_ICONS` (lucide component reference). Rendered as "Commenting on {label}". Without an entry you get "an unsupported event".
|
||||
- **`src/pages/NotificationsPage.tsx`** — `NOTIFICATION_KIND_NOUNS` (bare lowercase nouns: `'highlight'`, `'article'`). Rendered as "reacted to your {noun}". Without an entry you get "post" as a fallback.
|
||||
- **`src/components/NoteCard.tsx`** — `KIND_HEADER_MAP` (already covered in step 2).
|
||||
|
||||
### 6. Embedded previews (`src/components/EmbeddedNote.tsx`)
|
||||
|
||||
The quote-embed dispatcher in `EmbeddedNote` (around lines 65–110) routes kinds to dedicated compact cards. **Without a branch here, non-content kinds fall through to `EmbeddedNoteCard`, which either:**
|
||||
|
||||
- Shows only the NIP-31 `alt` tag (if present), or
|
||||
- Tombstones as "This event kind is not supported", or
|
||||
- **Feeds the event's `content` through the kind-1 tokenizer** if the kind is mistakenly treated as a content-kind — auto-linkifying URLs and hashtags that weren't authored by the event creator. This is a security/UX bug.
|
||||
|
||||
For any kind whose `content` isn't freeform user prose, add an explicit dispatch branch even if it just renders a minimal compact card. Pattern:
|
||||
|
||||
```tsx
|
||||
if (event.kind === 9802) {
|
||||
return <EmbeddedHighlightCard event={event} className={className} disableHoverCards={disableHoverCards} />;
|
||||
}
|
||||
```
|
||||
|
||||
Then define the compact card using `EmbeddedCardShell` for the author row + navigation, and render the kind-specific body inside. See `EmbeddedHighlightCard` and `EmbeddedBadgeAwardCard` for reference.
|
||||
|
||||
`src/components/EmbeddedNaddr.tsx` works similarly for addressable kinds — add a branch there if your kind is addressable.
|
||||
|
||||
### 7. Feed/sidebar registration (`src/lib/extraKinds.ts`)
|
||||
|
||||
Only needed if you decided on "feed-toggle + dedicated page" above. Add an `ExtraKindDef`:
|
||||
|
||||
```ts
|
||||
{
|
||||
kind: 9802,
|
||||
id: 'highlights',
|
||||
showKey: 'showHighlights',
|
||||
feedKey: 'feedIncludeHighlights',
|
||||
label: 'Highlights',
|
||||
description: 'Noteworthy excerpts from articles, posts, and the web (NIP-84)',
|
||||
route: 'highlights', // omit for feed-only registration
|
||||
addressable: false,
|
||||
section: 'social', // feed | media | social | development | whimsy
|
||||
blurb: 'Longer marketing copy shown in the info modal.',
|
||||
},
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
- **Sidebar icon** (`src/lib/sidebarItems.tsx`) — add `{ id: "highlights", label: "Highlights", path: "/highlights", icon: Highlighter }` to `SIDEBAR_ITEMS`, and import the icon at the top. `CONTENT_KIND_ICONS` picks up the icon automatically from the sidebar definition.
|
||||
- **Route** (`src/AppRouter.tsx`) — add `const highlightsDef = getExtraKindDef("highlights")!;` at the top of the file and a `<Route path="/highlights" element={<KindFeedPage kind={highlightsDef.kind} title={highlightsDef.label} icon={sidebarItemIcon("highlights", "size-5")} />} />` above the catch-all `*` route.
|
||||
|
||||
### 8. `AppConfig` triple (required if you added feed/sidebar toggle keys in step 7)
|
||||
|
||||
Three files must stay in sync, or the build fails or the setting silently no-ops:
|
||||
|
||||
1. **`src/contexts/AppContext.ts`** — add the fields to the `FeedSettings` interface with JSDoc comments.
|
||||
2. **`src/lib/schemas.ts`** — add the same fields to `FeedSettingsSchema` as `z.boolean().optional()`. `DittoConfigSchema` is derived from `AppConfigSchema` with `.strict()` mode, so any `ditto.json` field missing from Zod is a build error.
|
||||
3. **`src/App.tsx`** — add the default value in the initial `feedSettings` block.
|
||||
4. **`src/test/TestApp.tsx`** — mirror the default in test config so component tests work.
|
||||
|
||||
Convention: `show*` toggles default to `true` (sidebar entries visible), `feedInclude*` toggles default to `false` for niche content, `true` for core feed content.
|
||||
|
||||
### 9. Notification integration (if applicable)
|
||||
|
||||
Load this step when the kind represents an interaction with the user's content (reactions, reposts, highlights, awards, etc.) — i.e. when an event author "does something with" another user's content via an `e`/`a`/`p` tag.
|
||||
|
||||
**Six files** to update:
|
||||
|
||||
1. **`src/hooks/useEncryptedSettings.ts`** — add `highlights?: boolean` (or equivalent) to the `notificationPreferences` object.
|
||||
2. **`src/lib/notificationKinds.ts`** — add the kind to `ALL_NOTIFICATION_KINDS` and add a `if (p.X !== false) kinds.push(XXXX);` line in `getEnabledNotificationKinds`.
|
||||
3. **`src/lib/notificationTemplates.ts`** — add a `NOTIFICATION_TEMPLATES` entry with a title and body for nostr-push server-side notifications.
|
||||
4. **`src/pages/NotificationSettings.tsx`** — extend `NotificationPrefKey` union, add a row to `NOTIFICATION_TYPES` with icon/label/description/kinds.
|
||||
5. **`src/hooks/useNotifications.ts`** — extend `groupKey` (decide if events of this kind group by referenced event or stand alone), and if it's a "did something to your content" kind, add it to the author-ownership filter so users only get notified for interactions with their own content.
|
||||
6. **`src/pages/NotificationsPage.tsx`** — add a case to `GroupedNotificationView`'s switch; write `MyKindNotification` + `MyKindNotificationGroup` components modeled on `RepostNotification` / `LikeNotification`.
|
||||
|
||||
### 10. Spam guards (`src/lib/feedUtils.ts`)
|
||||
|
||||
If the kind has required tags (NIP-spec-mandated references, minimum content, etc.), add a check in `shouldHideFeedEvent` to hide events that don't meet the minimum bar. This pre-filters events before `NoteCard` mounts them, avoiding layout shifts from components that would return `null`.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
// NIP-84 highlights with no excerpt AND no source reference.
|
||||
if (event.kind === 9802) {
|
||||
const hasContent = event.content.trim().length > 0;
|
||||
const hasSource = event.tags.some(([n]) => n === 'a' || n === 'e' || n === 'r');
|
||||
if (!hasContent && !hasSource) return true;
|
||||
}
|
||||
```
|
||||
|
||||
### 11. `NIP.md` (custom kinds only)
|
||||
|
||||
If the kind is a Ditto-custom kind or a Ditto-specific extension of an existing NIP, document it in `NIP.md` — see the **`nostr-kind-design`** skill for the format. Standard NIPs (like NIP-84, NIP-23) do not go in `NIP.md`.
|
||||
|
||||
## Validation
|
||||
|
||||
After making changes, run `npm run test` — it runs `tsc --noEmit`, `eslint`, `vitest`, and `vite build` in sequence. All must pass. Additions to the `AppConfig` triple in particular frequently break the build if one of the four files is missed.
|
||||
|
||||
## Why so many locations?
|
||||
|
||||
These are genuinely different UI contexts (feed cards, detail pages, embedded previews, comment-context labels, notifications, sidebar routes) with different rendering requirements and grammar needs. The central `KIND_LABELS` in `src/lib/kindLabels.ts` handles the common "what to call this kind" case, but feed headers, comment-context text, and notification verbs each need their own grammar, and notification integration involves a whole independent subsystem.
|
||||
|
||||
## Bugs that signal a missed step
|
||||
|
||||
- **"Kind 12345" shown as a label** → step 4 (`KIND_LABELS`).
|
||||
- **"an unsupported event" in CommentContext** → step 5 (`CommentContext` maps).
|
||||
- **"reacted to your **post**"** when it should say "highlight" → step 5 (`NOTIFICATION_KIND_NOUNS`).
|
||||
- **No action header above a feed card** → step 2.4 (`KIND_HEADER_MAP`).
|
||||
- **Blank / `alt`-only card in quote embeds** → step 6 (`EmbeddedNote` dispatcher).
|
||||
- **URLs/hashtags in quoted text auto-linkified** → step 6 (embedded dispatcher forgot to bypass the kind-1 tokenizer).
|
||||
- **Kind doesn't appear in the home feed even with the toggle on** → step 7 (`ExtraKindDef` missing `feedKey`).
|
||||
- **Build error mentioning a missing `FeedSettings` field** → step 8 (one of the three files out of sync).
|
||||
- **Users not notified when their content is interacted with** → step 9 (notification stack).
|
||||
@@ -1,119 +0,0 @@
|
||||
---
|
||||
name: nostr-kinds
|
||||
description: Decide whether to reuse an existing NIP or mint a new kind, design tag structures that relays can index, choose what goes in content vs. tags, and register a new kind in Ditto's many UI touchpoints (feed cards, detail pages, embedded previews, kind-label maps).
|
||||
---
|
||||
|
||||
# Nostr Kinds — Design and Registration
|
||||
|
||||
Use this skill when introducing a new kind to Ditto, extending an existing NIP with new tags, or deciding whether an existing NIP covers a feature. It covers the decision framework, schema rules, and — critically — the full list of places a new kind must be registered in Ditto's UI.
|
||||
|
||||
## Choosing Between Existing NIPs and Custom Kinds
|
||||
|
||||
1. **Thorough NIP review first.** Browse the NIP index, then read candidate NIPs in detail. The goal is to find the closest existing solution.
|
||||
2. **Prefer extending existing NIPs** over creating custom kinds, even at the cost of minor schema compromises. Custom kinds fragment the ecosystem.
|
||||
3. **When an existing NIP is close but not perfect**, use its kind as the base and add domain-specific tags. Document the extension in `NIP.md`.
|
||||
4. **Only mint a new kind** when no existing NIP covers the core functionality, the data structure is fundamentally different, or the use case requires different storage characteristics (regular vs. replaceable vs. addressable).
|
||||
5. **If a tool to generate a new kind number is available, you MUST call it.** Never pick an arbitrary number.
|
||||
6. **Custom kinds MUST include a NIP-31 `alt` tag** with a human-readable description of the event's purpose.
|
||||
|
||||
**Example decision:**
|
||||
|
||||
```
|
||||
Need: Equipment marketplace for farmers
|
||||
Options:
|
||||
1. NIP-15 (Marketplace) — too structured for peer-to-peer sales
|
||||
2. NIP-99 (Classifieds) — good fit, extensible with farming tags
|
||||
3. Custom kind — perfect fit, no interoperability
|
||||
|
||||
Decision: NIP-99 + farming-specific tags.
|
||||
```
|
||||
|
||||
## Kind Ranges
|
||||
|
||||
An event's kind number determines storage semantics:
|
||||
|
||||
- **Regular** (1000 ≤ kind < 10000) — stored permanently by relays. Notes, articles, etc.
|
||||
- **Replaceable** (10000 ≤ kind < 20000) — only the latest event per `pubkey+kind` is kept. Profile metadata, contact lists, mute lists.
|
||||
- **Addressable** (30000 ≤ kind < 40000) — identified by `pubkey+kind+d-tag`; only the latest per combo is kept. Long-form content, products, definitions.
|
||||
|
||||
Kinds below 1000 are "legacy"; storage is per-kind (e.g. kind 1 is regular, kind 3 is replaceable).
|
||||
|
||||
## Tag Design Principles
|
||||
|
||||
- **Kind = schema, tags = semantics.** Don't mint a new kind just to represent a different category of the same data.
|
||||
- **Relays only index single-letter tags.** Use `t` for categories so filters like `'#t': ['electronics']` work at the relay level. Multi-letter tags (`product_type`, etc.) force inefficient client-side filtering.
|
||||
- **Filter at the relay**, not in JavaScript:
|
||||
|
||||
```ts
|
||||
// ❌ Fetch everything, filter locally
|
||||
const events = await nostr.query([{ kinds: [30402] }]);
|
||||
const filtered = events.filter((e) => hasTag(e, 'product_type', 'electronics'));
|
||||
|
||||
// ✅ Filter at the relay
|
||||
const events = await nostr.query([{ kinds: [30402], '#t': ['electronics'] }]);
|
||||
```
|
||||
|
||||
- **For Ditto-specific niches** (community apps, regional variants), tag events with a `t` value and query on it. Don't do this for generic platforms — it would silo content.
|
||||
|
||||
## Content vs. Tags
|
||||
|
||||
- **`content`** — large freeform text or existing industry-standard JSON (GeoJSON, FHIR, Tiled maps). Kind 0 is the one exception where structured JSON goes in content.
|
||||
- **Tags** — queryable metadata, structured data, anything you might filter on.
|
||||
- **Empty content is fine.** `content: ""` is idiomatic for tag-only events.
|
||||
- **If you need to filter by a field, it must be a tag** — relays don't index content.
|
||||
|
||||
```json
|
||||
// ✅ Queryable
|
||||
{ "kind": 30402, "content": "",
|
||||
"tags": [["d", "product-123"], ["title", "Camera"], ["price", "250"], ["t", "photography"]] }
|
||||
|
||||
// ❌ Structured data buried in content
|
||||
{ "kind": 30402, "content": "{\"title\":\"Camera\",\"price\":250}", "tags": [["d", "product-123"]] }
|
||||
```
|
||||
|
||||
## `NIP.md`
|
||||
|
||||
`NIP.md` documents Ditto's custom kinds and any extensions to existing NIPs. Whenever you mint a new kind or change a custom schema, **create or update `NIP.md`** with the tag list, content format, and intended usage. If a kind you add is effectively the same shape as an existing NIP, note the NIP reference rather than duplicating the spec.
|
||||
|
||||
## Registering a New Kind in the Ditto UI
|
||||
|
||||
When adding support for a new kind, the kind must be registered in **multiple locations** or it will render incorrectly in certain views (blank content in quote posts, "Kind 12345" as a label, missing action headers, etc.).
|
||||
|
||||
### Checklist
|
||||
|
||||
1. **Content card component** (`src/components/`) — create `<MyKindCard>` that renders the event's tags/content appropriately.
|
||||
|
||||
2. **Feed rendering** (`src/components/NoteCard.tsx`):
|
||||
- Add `const isMyKind = event.kind === XXXX;`.
|
||||
- Include it in the appropriate group flag (e.g. `isDevKind`) or the `isTextNote` exclusion list.
|
||||
- Add the content dispatch: `isMyKind ? <MyKindCard event={event} /> : …`.
|
||||
- Add an entry to `KIND_HEADER_MAP` for the action header (e.g. "deployed an nsite").
|
||||
- Import the new component and any new icons (e.g. `Globe` from `lucide-react`).
|
||||
|
||||
3. **Detail page** (`src/pages/PostDetailPage.tsx`):
|
||||
- Mirror the `isMyKind` detection and group/exclusion flags from `NoteCard`.
|
||||
- Add the content dispatch for the detail view.
|
||||
- `shellTitleForKind()` falls through to the central `KIND_LABELS` registry, so adding a label there is sufficient for the loading-state title. Only add a manual override in `shellTitleForKind()` if the kind belongs to a group (e.g. music kinds → "Track Details") or needs a composite label (e.g. "Badge Collection").
|
||||
- Import the new component.
|
||||
|
||||
4. **Feed registration** (`src/lib/extraKinds.ts`):
|
||||
- Add the kind number to an existing feed definition's `extraFeedKinds` array, or create a new `ExtraKindDef` entry.
|
||||
|
||||
5. **Central kind label registry** (`src/lib/kindLabels.ts`):
|
||||
- Add an entry to the `KIND_LABELS` map with a short, user-facing label (capitalized noun phrase, no articles).
|
||||
- This registry is the single source of truth for kind→label mappings and is consumed by the nsite permission prompt, signer nudge toasts, detail page loading titles, and addressable event preview headers.
|
||||
- Some UI contexts maintain **context-specific** label maps that cannot use the central registry directly (they need different grammar):
|
||||
- `KIND_LABELS` and `KIND_ICONS` in `src/components/CommentContext.tsx` — uses articles ("a post", "an article") for "Commenting on {label}" text.
|
||||
- `NOTIFICATION_KIND_NOUNS` in `src/pages/NotificationsPage.tsx` — uses bare lowercase nouns for notification action text.
|
||||
- `KIND_HEADER_MAP` in `src/components/NoteCard.tsx` — uses action verbs + nouns for feed headers.
|
||||
- These context-specific maps must also be updated when adding a new kind.
|
||||
|
||||
6. **Embedded note cards** (`src/components/EmbeddedNote.tsx`, `src/components/EmbeddedNaddr.tsx`) — small preview cards shown inside quote posts, reply-context indicators, and CommentContext hover cards. They are **separate components** from `NoteCard` and render a minimal preview (author + title/content + attachment indicators). Basic rendering works for all kinds automatically, but kinds whose media lives in tags (e.g. kind 20 photos via `imeta` tags) may need attachment-indicator logic added to `EmbeddedNoteCard`.
|
||||
|
||||
> Do not confuse these with the `compact` prop on `NoteCard` — that just hides action buttons on the full `NoteCard`. `EmbeddedNote`/`EmbeddedNaddr` are entirely different components.
|
||||
|
||||
7. **Reply composer** (`src/components/ReplyComposeModal.tsx`) — `EmbeddedPost` delegates to the shared `EmbeddedNote`/`EmbeddedNaddr` components, so no per-kind registration is needed here.
|
||||
|
||||
### Why so many places?
|
||||
|
||||
These are genuinely different UI contexts (feed cards, detail pages, embedded previews, reply previews, comment-context labels) with different rendering requirements. The central `KIND_LABELS` in `src/lib/kindLabels.ts` handles the common case, but several contexts need grammar-specific maps (articles, verbs, lowercase nouns) that can't be derived mechanically. **When in doubt, grep the codebase for an existing kind number like `30617`** — you'll find every registration point you need to mirror.
|
||||
@@ -25,7 +25,7 @@ Ditto is a Nostr client built with React 19.x, TailwindCSS 3.x, Vite, shadcn/ui,
|
||||
- `/public/` — static assets.
|
||||
- `App.tsx` — **already configured** with `QueryClientProvider`, `NostrProvider`, `UnheadProvider`, `AppProvider`, `NostrLoginProvider`, `NWCContext`. Read before editing; changes are rarely needed.
|
||||
- `AppRouter.tsx` — React Router configuration.
|
||||
- `NIP.md` — custom kinds documented by this project (see the `nostr-kinds` skill).
|
||||
- `NIP.md` — custom kinds documented by this project (see the `nostr-kind-design` skill).
|
||||
|
||||
**Always read an existing file before modifying it.** Never overwrite `App.tsx`, `AppRouter.tsx`, or `NostrProvider` without first reading their contents.
|
||||
|
||||
@@ -54,7 +54,10 @@ By default `nostr` uses the app's connection pool (reads from one relay, publish
|
||||
|
||||
### Kinds, Tags, and NIP.md
|
||||
|
||||
When introducing a new kind, extending an existing NIP with new tags, or registering a kind in the UI (feed cards, detail pages, embedded previews, kind-label maps), load the **`nostr-kinds`** skill. It covers the NIP-vs-custom-kind decision framework, kind ranges, tag design (single-letter indexed tags, content vs. tags), the `NIP.md` documentation requirement, and Ditto's multi-location UI registration checklist.
|
||||
Two skills split the work of working with kinds:
|
||||
|
||||
- **`nostr-kind-design`** — load when minting a new kind, extending an existing NIP with new tags, or deciding whether an existing NIP covers a use case. Covers the NIP-vs-custom decision framework, kind ranges, tag design (single-letter indexed tags, content vs. tags), and the `NIP.md` documentation requirement.
|
||||
- **`nostr-kind-rendering`** — load when adding UI for an event kind Ditto doesn't yet display, when asked to "support" / "display" / "render" a specific NIP or kind number, or when a kind renders blank / as "Kind 12345" / as "This event kind is not supported". Covers Ditto's multi-location UI registration checklist — feed cards, detail pages, embedded previews, kind-label maps (`KIND_LABELS`, `KIND_HEADER_MAP`, `NOTIFICATION_KIND_NOUNS`, `CommentContext`), notifications, routes, and the `AppConfig` triple that must stay in sync.
|
||||
|
||||
Summary rules:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user