- Port letter protocol (kind 8211, NIP-44 encrypted) from lief - LettersPage at /letters with inbox and sent tabs - ComposeLetterSheet with full stationery, font, frame, sticker, drawing support - LetterCard with expand-to-read animation and deletion - LetterPreferencesSection stored in encrypted settings (NIP-78) - /settings/letters route for letter preferences - Letters added to sidebar nav - All letter lib utilities: letterTypes, letterUtils, colorUtils extensions, sanitizeSvg, svgDrawing - StationeryBackground, StationeryPicker, FramePicker, StickerPicker, DrawingCanvas all ported
14 KiB
NIP: Custom Event Kinds
Event Kinds Overview
| Kind | Name | Description |
|---|---|---|
| 36767 | Theme Definition | Shareable, named custom UI theme |
| 16767 | Active Profile Theme | The user's currently active theme (one per user) |
| 16769 | Profile Tabs | The user's custom profile page tabs (one per user) |
| 8211 | Encrypted Letter | Encrypted personal letter with visual stationery |
Kind 36767: Theme Definition
Summary
Addressable event kind for publishing shareable custom UI themes. A single user may publish multiple themes, each identified by a unique d tag.
A theme consists of colors, optional fonts, and an optional background. Colors are stored in c tags, fonts in f tags, and background in a bg tag.
Event Structure
{
"kind": 36767,
"content": "",
"tags": [
["d", "mk-dark-theme"],
["c", "#1a1a2e", "background"],
["c", "#e0e0e0", "text"],
["c", "#6c3ce0", "primary"],
["f", "Inter", "https://example.com/inter.woff2", "body"],
["f", "Playfair Display", "https://example.com/playfair.woff2", "title"],
["bg", "url https://example.com/bg.jpg", "mode cover", "m image/jpeg", "dim 1920x1080"],
["title", "MK Dark Theme"],
["alt", "Custom theme: MK Dark Theme"]
]
}
Content
The content field is unused and MUST be an empty string ("").
Tags
| Tag | Required | Description |
|---|---|---|
d |
Yes | Unique identifier (slug) for this theme, e.g. "mk-dark-theme" |
c |
Yes (×3) | Hex color with marker. See Color Tags. |
f |
No | Font declaration. See Font Tag. |
bg |
No | Background media. See Background Tag. |
title |
Yes | Human-readable theme name |
alt |
Yes | NIP-31 human-readable fallback |
Multiple Themes Per User
Since kind 36767 is addressable, a user can publish multiple themes by using different d tag values. Publishing a new event with the same d tag replaces the previous version (this is how editing works).
Kind 16767: Active Profile Theme
Summary
Replaceable event that represents the user's currently active profile theme. Only one per user. When other users visit a profile, they query this kind to determine what theme to display.
Event Structure
{
"kind": 16767,
"content": "",
"tags": [
["c", "#1a1a2e", "background"],
["c", "#e0e0e0", "text"],
["c", "#6c3ce0", "primary"],
["f", "Inter", "https://example.com/inter.woff2", "body"],
["f", "Playfair Display", "https://example.com/playfair.woff2", "title"],
["bg", "url https://example.com/bg.jpg", "mode cover", "m image/jpeg"],
["title", "MK Dark Theme"],
["alt", "Active profile theme"]
]
}
Content
The content field is unused and MUST be an empty string ("").
Tags
| Tag | Required | Description |
|---|---|---|
c |
Yes (×3) | Hex color with marker. See Color Tags. |
f |
No | Font declaration. See Font Tag. |
bg |
No | Background media. See Background Tag. |
title |
No | Human-readable name for the theme |
alt |
Yes | NIP-31 human-readable fallback |
Client Behavior
- When visiting a profile, clients query
{ kinds: [16767], authors: [pubkey], limit: 1 }to get the active theme. - Clients read the
ctags to extract colors,ftags for fonts, andbgtag for the background. - Setting a new active theme publishes a new kind 16767 event (replacing the old one).
- To remove the active theme, publish a kind 5 deletion event targeting kind 16767.
Shared Tag Definitions
The following tag definitions apply to both kind 36767 and kind 16767.
Color Tags
Format: ["c", "#rrggbb", "<marker>"]
| Index | Required | Description |
|---|---|---|
| 0 | Yes | Tag name: "c" |
| 1 | Yes | Lowercase 6-digit hex color code including the # sign (e.g. "#ff0000") |
| 2 | Yes | Color role marker: one of "primary", "text", or "background" |
- All three markers (
"primary","text","background") MUST be present. - Only one
ctag per marker is allowed.
Font Tag
Format: ["f", "<family>", "<url>", "<role>"]
| Index | Required | Description |
|---|---|---|
| 0 | Yes | Tag name: "f" |
| 1 | Yes | CSS font-family name (e.g. "Inter") |
| 2 | Yes | Direct URL to a font file (.woff2, .ttf, .otf) |
| 3 | Yes | Font role: "body" or "title" |
Roles:
| Role | Applies to |
|---|---|
"body" |
All text globally (body, headings, UI elements) |
"title" |
The user's profile display name |
Rules:
- The
ftag is optional on the event. - At most one
ftag per role is allowed (i.e. one body font and one title font). - The
"body"font tag MUST be ordered before the"title"font tag. This ensures backward-compatible clients that only read the firstftag will pick up the body font. - If the URL fails to load, the client SHOULD fall back to a default font gracefully.
- Clients that do not recognize a role SHOULD ignore that
ftag. - Legacy events with an
ftag that has no role marker (only 3 elements) SHOULD be treated as"body". - Variable font files (covering multiple weights in a single file) are preferred.
Background Tag
The bg tag uses an imeta-style variadic format where each entry (after the tag name) is a space-delimited key/value pair.
Format: ["bg", "url <url>", "mode <mode>", "m <mime-type>", ...]
| Key | Required | Description |
|---|---|---|
url |
Yes | URL to an image or video file |
mode |
Yes | Display mode: "cover" or "tile" |
m |
Yes | MIME type (e.g. "image/jpeg", "image/png", "video/mp4") |
dim |
No | Dimensions in pixels: "<width>x<height>" (e.g. "1920x1080") |
blurhash |
No | Blurhash placeholder string for progressive loading |
- At most one
bgtag is allowed per event. - Clients MAY choose not to render video backgrounds for performance or bandwidth reasons.
- Unknown keys SHOULD be ignored for forward compatibility.
Kind 16769: Profile Tabs
Summary
Replaceable event kind for publishing a user's custom profile page tabs. Exactly one event per user (no d tag). Each tab defines a Nostr filter (NIP-01) that clients execute to populate the tab's content.
Visitors who load a profile fetch this event to display the custom tabs alongside the standard Posts / Media / Likes / Wall tabs.
Event Structure
{
"kind": 16769,
"content": "",
"tags": [
["var", "$follows", "p", "a:3:$me:"],
["tab", "Bitcoin Posts", "{\"kinds\":[1],\"authors\":[\"$me\"],\"search\":\"bitcoin\"}"],
["tab", "Feed", "{\"kinds\":[1,6],\"authors\":[\"$follows\"],\"limit\":40}"],
["alt", "Custom profile tabs"]
]
}
Content
The content field is unused and MUST be an empty string ("").
Tags
| Tag | Format | Description |
|---|---|---|
tab |
["tab", "<label>", "<filterJSON>"] |
One tag per custom tab. Order defines display order. |
var |
["var", "<$name>", "<tag>", "<pointer>"] |
Variable definition. See Variable Tags. |
alt |
["alt", "Custom profile tabs"] |
NIP-31 human-readable fallback. Required. |
Tab Filter JSON
The third element of each tab tag is a JSON-encoded NIP-01 filter object, optionally extended with the NIP-50 search field. Variable placeholders (strings starting with $) may appear wherever a string value is expected.
{
"kinds": [1],
"authors": ["$me"],
"search": "bitcoin",
"limit": 20
}
Supported filter fields: ids, authors, kinds, #<tag> (e.g. #t, #e, #p), since, until, limit, search.
Variable Tags
Variable tags define named placeholders that are resolved before the filter is executed. Each var tag extracts tag values from a referenced Nostr event.
Format: ["var", "$name", "<tag-to-extract>", "<event-pointer>"]
| Index | Description |
|---|---|
| 0 | Tag name: "var" |
| 1 | Variable name, starting with $ (e.g. "$follows") |
| 2 | Tag name to extract values from in the referenced event (e.g. "p") |
| 3 | Event pointer: e:<event-id> for a specific event, or a:<kind>:<pubkey>:<d-tag> for an addressable/replaceable event coordinate. Variables like $me may appear in the pubkey position. |
Example — extract follow list pubkeys:
["var", "$follows", "p", "a:3:$me:"]
This means: fetch the kind 3 event authored by $me, extract all p tag values, and bind them to $follows.
Reserved Variable: $me
The $me variable is the only runtime-provided variable. It resolves to the profile owner's pubkey (the author of the kind 16769 event). It does not require a var tag definition.
Variable Resolution
When a variable appears in a filter field that expects an array (e.g. authors, ids, #p), the variable is expanded in-place (spliced into the array). Literal values may be mixed with variables.
["tab", "Mixed", "{\"authors\":[\"$follows\",\"abc123...\"],\"kinds\":[1]}"]
After resolution (assuming $follows = ["pk1", "pk2"]):
{"authors": ["pk1", "pk2", "abc123..."], "kinds": [1]}
Behavior
- To add or update tabs: publish a new kind 16769 event with all current
tabandvartags. - To clear all tabs: publish a kind 16769 event with no
tabtags (onlyalt). - Clients MUST filter by
authors: [pubkey]when querying to prevent spoofing. vartags are shared across alltabtags in the same event.
Kind 0 Extension: Avatar Shape
Summary
An optional shape property on kind 0 (profile metadata) that controls how the user's avatar is masked/clipped when displayed. The value is an emoji character whose silhouette is used as a mask over the avatar image. When absent, the avatar renders as the standard circle.
Metadata Field
The shape field is added to the JSON content of a kind 0 event alongside standard fields like name, picture, etc. Its value is a single emoji character (including multi-codepoint emoji such as flags, ZWJ sequences, and skin-tone variants).
{
"kind": 0,
"content": "{\"name\":\"Luna\",\"picture\":\"https://example.com/luna.jpg\",\"shape\":\"🌙\"}"
}
Client Behavior
- When
shapeis absent, clients SHOULD render the avatar as a circle (the current universal default). - When
shapeis a valid emoji, clients SHOULD use the emoji's silhouette as an alpha mask over the avatar image. The specific rendering technique is platform-dependent (see below). - When
shapeis set to an unrecognized or invalid value, clients MUST fall back to a circle. This ensures forward compatibility. - The
shapefield is purely cosmetic and has no protocol-level significance. - Clients MAY choose not to support this extension, in which case avatars render as circles as usual.