Threads have no editable, shared subject, feed rows are titled by kind, Inbox rows have no headline
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
**Motivation**
A thread in Buzz has no name anyone can set, so every surface that wants to title one invents something.
- **The Home feed titles rows by kind.** `feedHeadline()` (`desktop/src/features/home/ui/FeedSection.tsx:61`) is a pure kind→string switch rendered as the row headline at `:196` — `"Forum post"`, `"Forum reply"`, `"Mention"`, `"Channel update"`. Ten conversations render as ten rows reading "Mention" / "Channel update".
- **The Inbox has no headline at all** — sender, a type label from `getInboxTypeLabel()`, then the raw message preview (`InboxListPane.tsx:306`, `:443`). Nothing says what the conversation is *about*.
- **Someone already started this and it never landed.** A second `feedHeadline()` at `desktop/src/features/home/lib/inbox.ts:137` does the right thing — `subject` tag → first line → fallback — and assigns it to `InboxItem.subject` (declared `:59`, computed `:591`, set `:621`). **Nothing renders it.** Those four lines are every `subject` hit under `features/home`.
Prior art: [NIP-14](https://github.com/nostr-protocol/nips/blob/master/14.md) defines a `subject` tag for threaded events, and Buzz uses `subject` on issues and PRs (`crates/buzz-sdk/src/builders.rs:1116`, `:1501`). What's missing is an **editable, shared, searchable** subject for kind:9 / 45001 threads. NIP-14's lives on the root, so it's write-once — `build_edit` (`builders.rs:389`) carries only `h`, `e` and content, and nothing in Buzz mutates a tag on a stored event — and tags aren't full-text indexed.
**Proposed solution**
A stored, user-signed, channel-scoped **parameterized-replaceable** kind whose **`content` is the subject text**.
- Kind in 30000–39999 so the NIP-33 replace path applies (`30180` looks free; `kind.rs` is authoritative).
- **`d` = the thread root event id** — the replacement key, so relabeling is just publishing again and old threads behave like new ones. **`e`** = the same root id, **`h`** = the channel. `replace_parameterized_event(community, &event, &d_tag, channel_id)` (`crates/buzz-relay/src/handlers/ingest.rs:3153`) already takes a `channel_id`, so channel-scoped 30xxx is supported — existing 30xxx kinds just haven't used it that way.
- **Subject in `content`, not a tag.** The FTS generated column indexes `content` only (`migrations/0008_fresh_install_search_allowlist.sql:15`). A tag would be permanently unsearchable — which is why issue and PR subjects aren't full-text searchable today.
Unlabeled stays first-class: no prompt, no empty-state nag, no required field. Plenty of channels need no labels at all.
**This needs a dedicated ingest validator** — the generic NIP-33 path enforces none of it. Invariants: exact tag cardinality and `d == e`, both valid 64-hex; target event exists, is **top-level**, and belongs to the channel in `h`; `content` non-empty after trimming and ≤256 chars; plus an explicit authorization rule. Note that `check_channel_membership` (`handlers/ingest.rs:742`) returns `Ok(())` for **non-members in open channels**, while the closest precedent — the kind:9002 channel topic — calls `is_member_cached()` and rejects them outright (`handlers/side_effects.rs:623-633`).
**Decisions I'd like from a maintainer before writing code:**
1. **Authorization** — mirror kind:9002 (any member, `is_member_cached`), root author only, or channel owner/admin?
2. **Precedence** — NIP-33 keeps one event per `(pubkey, kind, d)`, so distinct authors survive side by side and clients need a deterministic pick. "Root author wins" breaks the case this feature is partly for: an agent that authors both root and subject *is* the root author, so its owning human could never override it. Client-side rule, or one relay-selected canonical value with an explicit writer list?
3. **Clearing** — non-empty content plus replace-only means there's no way back to unlabeled. NIP-09 delete of the coordinate, or a defined tombstone?
4. **Interop** — also write a NIP-14 `subject` tag on the root at creation? I'd default **no**: write-once, unsearchable in Buzz, and permanently divergent once the real subject is edited. Only worth it if a specific non-Buzz client is known to read `subject` on kinds 9/45001.
**Alternatives considered**
- **`subject` tag on the root (the NIP-14 shape).** Write-once, per above — can't label an existing thread, can't correct a bad label, can't be set by anyone but the author, and unsearchable.
- **Extend the 39005 thread summary.** Relay-only, synthesized at query time, never stored — and absent entirely for zero-reply threads (`api/bridge.rs:539-543`), which is exactly the state a freshly created, freshly labeled thread is in.
- **Client-local titles.** #6350 (draft, pinned Thread Rail) has per-identity local titles — fine for one person's rail, but not shared, not signed, invisible to other members and to agents.
**Additional context**
Known required work — this is not a one-file change, which is why I'd rather agree the shape here than open a speculative PR:
| area | work |
|---|---|
| `buzz-core` | kind constant, `ALL_KINDS` |
| relay ingest | the dedicated validator above |
| relay bridge | add the kind to `WINDOW_AUX_KINDS` (`api/bridge.rs:384-389`) — it's a closed 4-kind list, so an `e` tag alone gets a new kind nothing and a subject would store fine then vanish on reload |
| migrations | **new additive** migration for the FTS allowlist (0007/0008 are applied on running relays and their sqlx checksums are immutable — `crates/buzz-db/src/migration.rs:772-773`), plus `scripts/maintenance/nip_rs_search_allowlist.sql` for populated installs |
| desktop | aux parsing, render, edit affordance, search kinds (`src-tauri/src/commands/messages.rs:178` hard-codes `[9, 40002, 45001, 45003]`), and result→root mapping so a hit opens the thread rather than the metadata event |
| mobile | aux parsing, render, search kinds (`mobile/lib/features/search/search_provider.dart:139`) |
| docs | `docs/nips/NIP-**.md`, `kind.rs` |
I have the full tracing behind the migration and search rows available if it's useful.
**Related:** #6067 (working set of labelled items — a shared signed subject would be an input, not a competitor); #4266 and draft PR #6350 (thread rail); #5254 (search/filter threads in a forum channel).
Searched open issues and PRs for `topic`, `subject`, `rename`, and `thread title` before filing; the only `topic` hits are channel-level (`set_channel_topic`). Point me at a duplicate and I'll close this.
Happy to implement once the four questions are settled.
Contributor guide
Assessment
This issue has not been assessed yet.