Browse Source

Add event template generator with 140+ Nostr event kinds (v0.36.2)

- Add comprehensive eventKinds.js database with all NIPs event kinds
  including templates, descriptions, NIP references, and type flags
- Create EventTemplateSelector.svelte modal with search functionality
  and category filtering (Social, Messaging, Lists, Marketplace, etc.)
- Update ComposeView with "Generate Template" button and error banner
  for displaying permission-aware publish error messages
- Enhance publishEvent() in App.svelte with detailed error handling
  that explains policy restrictions, permission issues, and provides
  actionable guidance for users
- Add permission pre-check to prevent read-only users from attempting
  to publish events
- Update CLAUDE.md with Web UI event templates documentation
- Create docs/WEB_UI_EVENT_TEMPLATES.md with comprehensive user guide

Files modified:
- app/web/src/eventKinds.js (new)
- app/web/src/EventTemplateSelector.svelte (new)
- app/web/src/ComposeView.svelte
- app/web/src/App.svelte
- docs/WEB_UI_EVENT_TEMPLATES.md (new)
- CLAUDE.md
- pkg/version/version

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
main
mleku 4 weeks ago
parent
commit
0a3e639fee
No known key found for this signature in database
  1. 25
      CLAUDE.md
  2. 95
      app/web/src/App.svelte
  3. 116
      app/web/src/ComposeView.svelte
  4. 404
      app/web/src/EventTemplateSelector.svelte
  5. 2644
      app/web/src/eventKinds.js
  6. 163
      docs/WEB_UI_EVENT_TEMPLATES.md
  7. 2
      pkg/version/version

25
CLAUDE.md

@ -334,6 +334,11 @@ export ORLY_AUTH_TO_WRITE=false # Require auth only for writes
- Features: event browser with advanced filtering, sprocket management, policy management, user admin, settings - Features: event browser with advanced filtering, sprocket management, policy management, user admin, settings
- **Event Browser:** Enhanced filter system with kind, author, tag, and time range filters (replaced simple search) - **Event Browser:** Enhanced filter system with kind, author, tag, and time range filters (replaced simple search)
- **Policy Management Tab:** JSON editor with validation, save publishes kind 12345 event - **Policy Management Tab:** JSON editor with validation, save publishes kind 12345 event
- **Compose Tab with Event Templates:** Generate pre-filled event templates for all 140+ Nostr event kinds
- `eventKinds.js` - Comprehensive database of event kinds from NIPs with templates
- `EventTemplateSelector.svelte` - Scrolling modal with search and category filtering
- Category filters: All, Regular, Replaceable, Ephemeral, Addressable, Social, Messaging, Lists, Marketplace, Lightning, Media, Git, Calendar, Groups
- Permission-aware error messages explaining policy/role restrictions when publishing fails
**Command-line Tools (`cmd/`):** **Command-line Tools (`cmd/`):**
- `relay-tester/` - Nostr protocol compliance testing - `relay-tester/` - Nostr protocol compliance testing
@ -816,7 +821,7 @@ Files modified:
3. GitHub Actions workflow builds binaries for multiple platforms 3. GitHub Actions workflow builds binaries for multiple platforms
4. Release created automatically with binaries and checksums 4. Release created automatically with binaries and checksums
## Recent Features (v0.34.x) ## Recent Features (v0.34.x - v0.36.x)
### Directory Spider ### Directory Spider
The directory spider (`pkg/spider/directory.go`) automatically discovers and syncs metadata from other relays: The directory spider (`pkg/spider/directory.go`) automatically discovers and syncs metadata from other relays:
@ -860,6 +865,22 @@ WebAssembly-compatible database backend (`pkg/wasmdb/`):
- **Reference Documentation**: `docs/POLICY_CONFIGURATION_REFERENCE.md` provides authoritative read vs write applicability - **Reference Documentation**: `docs/POLICY_CONFIGURATION_REFERENCE.md` provides authoritative read vs write applicability
- See also: `pkg/policy/README.md` for quick reference - See also: `pkg/policy/README.md` for quick reference
### Web UI Event Templates (v0.36.x)
The Compose tab now includes a comprehensive event template generator:
- **140+ Event Kinds**: Complete database of Nostr event kinds from the NIPs repository
- **Template Generator**: Click "Generate Template" to open searchable modal with all event types
- **Category Filtering**: Filter by Regular, Replaceable, Ephemeral, Addressable, or domain-specific categories (Social, Messaging, Lists, Marketplace, Lightning, Media, Git, Calendar, Groups)
- **Search**: Find events by name, description, kind number, or NIP reference
- **Pre-filled Templates**: Each kind includes proper tag structure and example content
- **Permission-Aware Errors**: When publishing fails, error messages explain:
- Policy restrictions (kind blocked, content limits)
- Permission issues (user role insufficient)
- Guidance on how to resolve (contact admin, policy config)
- **Key Files**:
- `app/web/src/eventKinds.js` - Event kinds database with templates
- `app/web/src/EventTemplateSelector.svelte` - Template selection modal
- `app/web/src/ComposeView.svelte` - Updated compose interface
### Policy JSON Configuration Quick Reference ### Policy JSON Configuration Quick Reference
```json ```json
@ -956,4 +977,6 @@ Invite-based access control system:
| `pkg/neo4j/MODIFYING_SCHEMA.md` | How to modify Neo4j schema | | `pkg/neo4j/MODIFYING_SCHEMA.md` | How to modify Neo4j schema |
| `pkg/neo4j/TESTING.md` | Neo4j testing guide | | `pkg/neo4j/TESTING.md` | Neo4j testing guide |
| `.claude/skills/cypher/SKILL.md` | Cypher query language skill for Neo4j | | `.claude/skills/cypher/SKILL.md` | Cypher query language skill for Neo4j |
| `app/web/src/eventKinds.js` | Comprehensive Nostr event kinds database (140+ kinds with templates) |
| `docs/WEB_UI_EVENT_TEMPLATES.md` | Web UI event template generator documentation |
| `readme.adoc` | Project README with feature overview | | `readme.adoc` | Project README with feature overview |

95
app/web/src/App.svelte

@ -117,6 +117,7 @@
// Compose tab state // Compose tab state
let composeEventJson = ""; let composeEventJson = "";
let composePublishError = "";
// Recovery tab state // Recovery tab state
let recoverySelectedKind = null; let recoverySelectedKind = null;
@ -2562,31 +2563,42 @@
} }
async function publishEvent() { async function publishEvent() {
// Clear any previous errors
composePublishError = "";
try { try {
if (!composeEventJson.trim()) { if (!composeEventJson.trim()) {
alert("Please enter an event to publish"); composePublishError = "Please enter an event to publish";
return; return;
} }
if (!isLoggedIn) { if (!isLoggedIn) {
alert("Please log in to publish events"); composePublishError = "Please log in to publish events";
return; return;
} }
if (!userSigner) { if (!userSigner) {
alert( composePublishError = "No signer available. Please log in with a valid authentication method.";
"No signer available. Please log in with a valid authentication method.",
);
return; return;
} }
const event = JSON.parse(composeEventJson); let event;
try {
event = JSON.parse(composeEventJson);
} catch (parseError) {
composePublishError = `Invalid JSON: ${parseError.message}`;
return;
}
// Validate that the event has required fields // Validate that the event has required fields
if (!event.id || !event.sig) { if (!event.id || !event.sig) {
alert( composePublishError = 'Event must be signed before publishing. Please click "Sign" first.';
'Event must be signed before publishing. Please click "Sign" first.', return;
); }
// Pre-check: validate user has write permission
if (userRole === "read") {
composePublishError = `Permission denied: Your current role is "${userRole}" which does not allow publishing events. Contact a relay administrator to upgrade your permissions.`;
return; return;
} }
@ -2602,18 +2614,70 @@
); );
if (result.success) { if (result.success) {
composePublishError = "";
alert("Event published successfully to ORLY relay!"); alert("Event published successfully to ORLY relay!");
// Optionally clear the editor after successful publish // Optionally clear the editor after successful publish
// composeEventJson = ''; // composeEventJson = '';
} else { } else {
alert( // Parse the error reason and provide helpful guidance
`Event publishing failed: ${result.reason || "Unknown error"}`, const reason = result.reason || "Unknown error";
); composePublishError = formatPublishError(reason, event.kind);
} }
} catch (error) { } catch (error) {
console.error("Error publishing event:", error); console.error("Error publishing event:", error);
alert("Error publishing event: " + error.message); const errorMsg = error.message || "Unknown error";
composePublishError = formatPublishError(errorMsg, null);
}
}
// Helper function to format publish errors with helpful guidance
function formatPublishError(reason, eventKind) {
const lowerReason = reason.toLowerCase();
// Check for policy-related errors
if (lowerReason.includes("policy") || lowerReason.includes("blocked") || lowerReason.includes("denied")) {
let msg = `Policy Error: ${reason}`;
if (eventKind !== null) {
msg += `\n\nKind ${eventKind} may be restricted by the relay's policy configuration.`;
}
if (policyEnabled) {
msg += "\n\nThe relay has policy enforcement enabled. Contact a relay administrator to allow this event kind or adjust your permissions.";
}
return msg;
}
// Check for permission/auth errors
if (lowerReason.includes("auth") || lowerReason.includes("permission") || lowerReason.includes("unauthorized")) {
return `Permission Error: ${reason}\n\nYour current permissions may not allow publishing this type of event. Current role: ${userRole || "unknown"}. Contact a relay administrator to upgrade your permissions.`;
}
// Check for kind-specific restrictions
if (lowerReason.includes("kind") || lowerReason.includes("not allowed") || lowerReason.includes("restricted")) {
let msg = `Event Type Error: ${reason}`;
if (eventKind !== null) {
msg += `\n\nKind ${eventKind} is not currently allowed on this relay.`;
}
msg += "\n\nThe relay administrator may need to update the policy configuration to allow this event kind.";
return msg;
}
// Check for rate limiting
if (lowerReason.includes("rate") || lowerReason.includes("limit") || lowerReason.includes("too many")) {
return `Rate Limit Error: ${reason}\n\nPlease wait a moment before trying again.`;
} }
// Check for size limits
if (lowerReason.includes("size") || lowerReason.includes("too large") || lowerReason.includes("content")) {
return `Size Limit Error: ${reason}\n\nThe event may exceed the relay's size limits. Try reducing the content length.`;
}
// Default error message
return `Publishing failed: ${reason}`;
}
// Clear the compose publish error
function clearComposeError() {
composePublishError = "";
} }
// Persist selected tab to local storage // Persist selected tab to local storage
@ -2720,9 +2784,14 @@
{:else if selectedTab === "compose"} {:else if selectedTab === "compose"}
<ComposeView <ComposeView
bind:composeEventJson bind:composeEventJson
{userPubkey}
{userRole}
{policyEnabled}
publishError={composePublishError}
on:reformatJson={reformatJson} on:reformatJson={reformatJson}
on:signEvent={signEvent} on:signEvent={signEvent}
on:publishEvent={publishEvent} on:publishEvent={publishEvent}
on:clearError={clearComposeError}
/> />
{:else if selectedTab === "managed-acl"} {:else if selectedTab === "managed-acl"}
<div class="managed-acl-view"> <div class="managed-acl-view">

116
app/web/src/ComposeView.svelte

@ -1,9 +1,17 @@
<script> <script>
export let composeEventJson = ""; export let composeEventJson = "";
export let userPubkey = "";
export let userRole = "";
export let policyEnabled = false;
export let publishError = "";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import EventTemplateSelector from "./EventTemplateSelector.svelte";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let isTemplateSelectorOpen = false;
function reformatJson() { function reformatJson() {
dispatch("reformatJson"); dispatch("reformatJson");
} }
@ -15,10 +23,32 @@
function publishEvent() { function publishEvent() {
dispatch("publishEvent"); dispatch("publishEvent");
} }
function openTemplateSelector() {
isTemplateSelectorOpen = true;
}
function handleTemplateSelect(event) {
const { kind, template } = event.detail;
composeEventJson = JSON.stringify(template, null, 2);
dispatch("templateSelected", { kind, template });
}
function handleTemplateSelectorClose() {
isTemplateSelectorOpen = false;
}
function clearError() {
publishError = "";
dispatch("clearError");
}
</script> </script>
<div class="compose-view"> <div class="compose-view">
<div class="compose-header"> <div class="compose-header">
<button class="compose-btn template-btn" on:click={openTemplateSelector}
>Generate Template</button
>
<button class="compose-btn reformat-btn" on:click={reformatJson} <button class="compose-btn reformat-btn" on:click={reformatJson}
>Reformat</button >Reformat</button
> >
@ -27,16 +57,34 @@
>Publish</button >Publish</button
> >
</div> </div>
{#if publishError}
<div class="error-banner">
<div class="error-content">
<span class="error-icon">&#9888;</span>
<span class="error-message">{publishError}</span>
</div>
<button class="error-dismiss" on:click={clearError}>&times;</button>
</div>
{/if}
<div class="compose-editor"> <div class="compose-editor">
<textarea <textarea
bind:value={composeEventJson} bind:value={composeEventJson}
class="compose-textarea" class="compose-textarea"
placeholder="Enter your Nostr event JSON here..." placeholder="Enter your Nostr event JSON here, or click 'Generate Template' to start with a template..."
spellcheck="false" spellcheck="false"
></textarea> ></textarea>
</div> </div>
</div> </div>
<EventTemplateSelector
bind:isOpen={isTemplateSelectorOpen}
{userPubkey}
on:select={handleTemplateSelect}
on:close={handleTemplateSelectorClose}
/>
<style> <style>
.compose-view { .compose-view {
position: fixed; position: fixed;
@ -71,6 +119,16 @@
background: var(--button-hover-bg); background: var(--button-hover-bg);
} }
.template-btn {
background: var(--primary);
color: var(--text-color);
}
.template-btn:hover {
background: var(--primary);
filter: brightness(0.9);
}
.reformat-btn { .reformat-btn {
background: var(--info); background: var(--info);
color: var(--text-color); color: var(--text-color);
@ -101,6 +159,53 @@
filter: brightness(0.9); filter: brightness(0.9);
} }
.error-banner {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75em 1em;
margin: 0 0.5em;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 0.25rem;
color: #721c24;
}
:global(.dark-theme) .error-banner {
background: #4a1c24;
border-color: #6a2c34;
color: #f8d7da;
}
.error-content {
display: flex;
align-items: center;
gap: 0.5em;
}
.error-icon {
font-size: 1.2em;
}
.error-message {
font-size: 0.9rem;
line-height: 1.4;
}
.error-dismiss {
background: none;
border: none;
font-size: 1.25rem;
cursor: pointer;
color: inherit;
padding: 0 0.25em;
opacity: 0.7;
}
.error-dismiss:hover {
opacity: 1;
}
.compose-editor { .compose-editor {
flex: 1; flex: 1;
display: flex; display: flex;
@ -137,5 +242,14 @@
.compose-view { .compose-view {
left: 160px; left: 160px;
} }
.compose-header {
flex-wrap: wrap;
}
.compose-btn {
flex: 1;
min-width: calc(50% - 0.5em);
}
} }
</style> </style>

404
app/web/src/EventTemplateSelector.svelte

@ -0,0 +1,404 @@
<script>
import { createEventDispatcher } from "svelte";
import { eventKinds, kindCategories, createTemplateEvent, searchEventKinds } from "./eventKinds.js";
export let isOpen = false;
export let userPubkey = "";
const dispatch = createEventDispatcher();
let searchQuery = "";
let selectedCategory = "all";
let filteredKinds = eventKinds;
// Filter kinds based on search and category
$: {
let kinds = eventKinds;
// Apply category filter
const category = kindCategories.find(c => c.id === selectedCategory);
if (category) {
kinds = kinds.filter(category.filter);
}
// Apply search filter
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
kinds = kinds.filter(k =>
k.name.toLowerCase().includes(query) ||
k.description.toLowerCase().includes(query) ||
k.kind.toString().includes(query) ||
(k.nip && k.nip.includes(query))
);
}
filteredKinds = kinds;
}
function selectKind(kindInfo) {
const template = createTemplateEvent(kindInfo.kind, userPubkey);
dispatch("select", {
kind: kindInfo,
template: template
});
closeModal();
}
function closeModal() {
isOpen = false;
searchQuery = "";
selectedCategory = "all";
dispatch("close");
}
function handleKeydown(event) {
if (event.key === "Escape") {
closeModal();
}
}
function handleBackdropClick(event) {
if (event.target === event.currentTarget) {
closeModal();
}
}
function getKindBadgeClass(kindInfo) {
if (kindInfo.isAddressable) return "badge-addressable";
if (kindInfo.isReplaceable) return "badge-replaceable";
if (kindInfo.kind >= 20000 && kindInfo.kind < 30000) return "badge-ephemeral";
return "badge-regular";
}
function getKindBadgeText(kindInfo) {
if (kindInfo.isAddressable) return "Addressable";
if (kindInfo.isReplaceable) return "Replaceable";
if (kindInfo.kind >= 20000 && kindInfo.kind < 30000) return "Ephemeral";
return "Regular";
}
</script>
<svelte:window on:keydown={handleKeydown} />
{#if isOpen}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="modal-backdrop" on:click={handleBackdropClick}>
<div class="modal-content">
<div class="modal-header">
<h2>Generate Event Template</h2>
<button class="close-btn" on:click={closeModal}>&times;</button>
</div>
<div class="modal-filters">
<div class="search-box">
<input
type="text"
placeholder="Search by name, description, or kind number..."
bind:value={searchQuery}
class="search-input"
/>
</div>
<div class="category-tabs">
{#each kindCategories as category}
<button
class="category-tab"
class:active={selectedCategory === category.id}
on:click={() => selectedCategory = category.id}
>
{category.name}
</button>
{/each}
</div>
</div>
<div class="modal-body">
<div class="kinds-list">
{#if filteredKinds.length === 0}
<div class="no-results">
No event kinds found matching "{searchQuery}"
</div>
{:else}
{#each filteredKinds as kindInfo}
<button
class="kind-item"
on:click={() => selectKind(kindInfo)}
>
<div class="kind-header">
<span class="kind-number">Kind {kindInfo.kind}</span>
<span class="kind-badge {getKindBadgeClass(kindInfo)}">
{getKindBadgeText(kindInfo)}
</span>
{#if kindInfo.nip && kindInfo.nip !== "XX"}
<span class="nip-badge">NIP-{kindInfo.nip}</span>
{/if}
</div>
<div class="kind-name">{kindInfo.name}</div>
<div class="kind-description">{kindInfo.description}</div>
</button>
{/each}
{/if}
</div>
</div>
<div class="modal-footer">
<span class="result-count">{filteredKinds.length} event type{filteredKinds.length !== 1 ? 's' : ''}</span>
<button class="cancel-btn" on:click={closeModal}>Cancel</button>
</div>
</div>
</div>
{/if}
<style>
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: var(--card-bg);
border-radius: 0.5rem;
width: 90%;
max-width: 800px;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
border: 1px solid var(--border-color);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.modal-header h2 {
margin: 0;
font-size: 1.25rem;
color: var(--text-color);
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-color);
padding: 0;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
}
.close-btn:hover {
background: var(--button-hover-bg);
}
.modal-filters {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.search-box {
margin-bottom: 0.75rem;
}
.search-input {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: 0.25rem;
background: var(--input-bg);
color: var(--input-text-color);
font-size: 0.9rem;
}
.search-input:focus {
outline: none;
border-color: var(--accent-color);
}
.category-tabs {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.category-tab {
padding: 0.4rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 1rem;
background: transparent;
color: var(--text-color);
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.category-tab:hover {
background: var(--button-hover-bg);
}
.category-tab.active {
background: var(--accent-color);
border-color: var(--accent-color);
color: white;
}
.modal-body {
flex: 1;
overflow-y: auto;
padding: 1rem 1.5rem;
}
.kinds-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.kind-item {
display: block;
width: 100%;
text-align: left;
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
background: var(--bg-color);
cursor: pointer;
transition: all 0.2s;
}
.kind-item:hover {
border-color: var(--accent-color);
background: var(--button-hover-bg);
}
.kind-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.kind-number {
font-family: monospace;
font-size: 0.8rem;
color: var(--accent-color);
font-weight: bold;
}
.kind-badge {
font-size: 0.65rem;
padding: 0.15rem 0.4rem;
border-radius: 0.25rem;
font-weight: 500;
}
.badge-regular {
background: #6c757d;
color: white;
}
.badge-replaceable {
background: #17a2b8;
color: white;
}
.badge-ephemeral {
background: #ffc107;
color: black;
}
.badge-addressable {
background: #28a745;
color: white;
}
.nip-badge {
font-size: 0.65rem;
padding: 0.15rem 0.4rem;
border-radius: 0.25rem;
background: var(--primary);
color: var(--text-color);
}
.kind-name {
font-weight: 600;
color: var(--text-color);
margin-bottom: 0.25rem;
}
.kind-description {
font-size: 0.85rem;
color: var(--text-color);
opacity: 0.7;
}
.no-results {
text-align: center;
padding: 2rem;
color: var(--text-color);
opacity: 0.6;
}
.modal-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-top: 1px solid var(--border-color);
}
.result-count {
font-size: 0.85rem;
color: var(--text-color);
opacity: 0.7;
}
.cancel-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--border-color);
border-radius: 0.25rem;
background: var(--button-bg);
color: var(--button-text);
cursor: pointer;
font-size: 0.9rem;
}
.cancel-btn:hover {
background: var(--button-hover-bg);
}
@media (max-width: 640px) {
.modal-content {
width: 95%;
max-height: 90vh;
}
.category-tabs {
overflow-x: auto;
flex-wrap: nowrap;
padding-bottom: 0.5rem;
}
.category-tab {
white-space: nowrap;
}
}
</style>

2644
app/web/src/eventKinds.js

File diff suppressed because it is too large Load Diff

163
docs/WEB_UI_EVENT_TEMPLATES.md

@ -0,0 +1,163 @@
# Web UI Event Templates
The ORLY Web UI includes a comprehensive event template generator that helps users create properly-structured Nostr events for any of the 140+ defined event kinds.
## Overview
The Compose tab provides a "Generate Template" button that opens a searchable, categorized modal dialog. Users can browse or search for any Nostr event kind and instantly load a pre-filled template with the correct structure, tags, and example content.
## Features
### Event Kind Database (`app/web/src/eventKinds.js`)
A comprehensive JavaScript database containing:
- **140+ event kinds** from the NIPs (Nostr Implementation Possibilities) repository
- Each entry includes:
- Kind number
- Human-readable name
- Description
- NIP reference (where applicable)
- Event type flags (replaceable, addressable, ephemeral)
- Pre-built template with proper tag structure
### Template Selector Modal (`app/web/src/EventTemplateSelector.svelte`)
A user-friendly modal interface featuring:
- **Search functionality**: Find events by name, description, kind number, or NIP reference
- **Category filters**: Quick-filter buttons for event types:
- All Kinds
- Regular Events (0-9999)
- Replaceable (10000-19999)
- Ephemeral (20000-29999)
- Addressable (30000-39999)
- Domain-specific: Social, Messaging, Lists, Marketplace, Lightning, Media, Git, Calendar, Groups
- **Visual badges**: Color-coded indicators showing event type
- **NIP references**: Quick reference to the defining NIP
- **Keyboard navigation**: Escape key closes the modal
### Permission-Aware Error Handling
When publishing fails, the system provides detailed, actionable error messages:
| Error Type | Description | User Guidance |
|------------|-------------|---------------|
| Policy Error | Event kind blocked by relay policy | Contact relay administrator to allow the kind |
| Permission Error | User role insufficient | Shows current role, suggests permission upgrade |
| Kind Restriction | Event type not allowed | Policy configuration may need updating |
| Rate Limit | Too many requests | Wait before retrying |
| Size Limit | Event too large | Reduce content length |
## Usage
### Generating a Template
1. Navigate to the **Compose** tab in the Web UI
2. Click the **Generate Template** button (purple button)
3. In the modal:
- Use the search box to find specific event types
- Or click category tabs to filter by event type
- Click on any event kind to select it
4. The template is loaded into the editor with:
- Correct `kind` value
- Proper tag structure with placeholder values
- Example content (where applicable)
- Current timestamp
- Your pubkey (if logged in)
### Editing and Publishing
1. Replace placeholder values (marked with `<angle_brackets>`) with actual data
2. Click **Reformat** to clean up JSON formatting
3. Click **Sign** to sign the event with your key
4. Click **Publish** to send to the relay
### Understanding Templates
Templates use placeholder values in angle brackets that must be replaced:
```json
{
"kind": 1,
"content": "Your note content here",
"tags": [
["p", "<pubkey_to_mention>"],
["e", "<event_id_to_reference>"]
],
"created_at": 1702857600,
"pubkey": "<your_pubkey_here>"
}
```
## Event Categories
### Regular Events (0-9999)
Standard events that are stored indefinitely. Examples:
- Kind 0: User Metadata
- Kind 1: Short Text Note
- Kind 7: Reaction
- Kind 1984: Reporting
### Replaceable Events (10000-19999)
Events where only the latest version is kept. Examples:
- Kind 10000: Mute List
- Kind 10002: Relay List Metadata
- Kind 13194: Wallet Info
### Ephemeral Events (20000-29999)
Events not intended for permanent storage. Examples:
- Kind 22242: Client Authentication
- Kind 24133: Nostr Connect
### Addressable Events (30000-39999)
Parameterized replaceable events identified by kind + pubkey + d-tag. Examples:
- Kind 30023: Long-form Content
- Kind 30311: Live Event
- Kind 34550: Community Definition
## API Reference
### Helper Functions in `eventKinds.js`
```javascript
import {
eventKinds, // Array of all event kinds
kindCategories, // Array of category filter definitions
getEventKind, // Get kind info by number
searchEventKinds, // Search by query string
createTemplateEvent // Generate template with current timestamp
} from './eventKinds.js';
// Get information about a specific kind
const kind1 = getEventKind(1);
// Returns: { kind: 1, name: "Short Text Note", description: "...", template: {...} }
// Search for kinds
const results = searchEventKinds("zap");
// Returns: Array of matching kinds
// Create a template event
const template = createTemplateEvent(1, "abc123...");
// Returns: Event object with current timestamp and provided pubkey
```
## Troubleshooting
### "Permission denied" error
Your user role does not allow publishing events. Check your role in the header badge and contact a relay administrator.
### "Policy Error: kind blocked"
The relay's policy configuration does not allow this event kind. If you're an administrator, check `ORLY_POLICY_PATH` or the Policy tab.
### "Event must be signed before publishing"
Click the **Sign** button before **Publish**. Events must be cryptographically signed before the relay will accept them.
### Template not loading
Ensure JavaScript is enabled and the page has fully loaded. Try refreshing the page.
## Related Documentation
- [POLICY_USAGE_GUIDE.md](./POLICY_USAGE_GUIDE.md) - Policy configuration for event restrictions
- [POLICY_CONFIGURATION_REFERENCE.md](./POLICY_CONFIGURATION_REFERENCE.md) - Policy rule reference
- [NIPs Repository](https://github.com/nostr-protocol/nips) - Official Nostr protocol specifications

2
pkg/version/version

@ -1 +1 @@
v0.36.1 v0.36.2

Loading…
Cancel
Save