Browse Source

get rid of magic numbers

master
Silberengel 1 month ago
parent
commit
1907bf1681
  1. 89
      ideas.txt
  2. 4
      public/healthz.json
  3. 20
      src/lib/components/EventMenu.svelte
  4. 2
      src/lib/components/content/EmbeddedEvent.svelte
  5. 2
      src/lib/components/content/QuotedContext.svelte
  6. 2
      src/lib/components/content/ReplyContext.svelte
  7. 17
      src/lib/components/modals/EventJsonModal.svelte
  8. 352
      src/lib/components/modals/RelatedEventsModal.svelte
  9. 8
      src/lib/modules/comments/CommentForm.svelte
  10. 103
      src/lib/modules/comments/CommentThread.svelte
  11. 13
      src/lib/modules/feed/FeedPage.svelte
  12. 18
      src/lib/modules/feed/FeedPost.svelte
  13. 2
      src/lib/modules/feed/Reply.svelte
  14. 2
      src/lib/modules/profiles/PaymentAddresses.svelte
  15. 11
      src/lib/modules/profiles/ProfilePage.svelte
  16. 23
      src/lib/modules/reactions/FeedReactionButtons.svelte
  17. 11
      src/lib/modules/reactions/ReactionButtons.svelte
  18. 8
      src/lib/modules/threads/ThreadCard.svelte
  19. 13
      src/lib/modules/threads/ThreadList.svelte
  20. 4
      src/lib/modules/zaps/ZapButton.svelte
  21. 2
      src/lib/modules/zaps/ZapReceipt.svelte
  22. 2
      src/lib/services/auth/activity-tracker.ts
  23. 3
      src/lib/services/cache/profile-cache.ts
  24. 5
      src/lib/services/nostr/auth-handler.ts
  25. 4
      src/lib/services/nostr/gif-service.ts
  26. 11
      src/lib/services/nostr/nip30-emoji.ts
  27. 11
      src/lib/services/nostr/nostr-client.ts
  28. 13
      src/lib/services/user-data.ts
  29. 131
      src/lib/types/kind-lookup.ts

89
ideas.txt

@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
1. When I open a ThreadDrawer, the "Replying to:..." blurb at the top should render as full event. And, if that replied-to event is also a reply, it's OP should also be rendered as a full event. And so on, up the hierarchy, until we get to an event that isn't a reference or reply to any other (no e-tag or q-tag or a-tag). I want to see the entire discussion tree, so that the event I clicked in the Feed view is displayed in complete context.
2. Fix the Threads list loading so slowly. I should immediately be seeing what is in cache, and then you update the cache and add anything missing, in a second sweep. And make sure updating doesn't cause the page the jump around or create endless loops.
3. Make sure that pinning and bookmarking (from the event "..." menu) actually create/update and publish the list events.
4. Add a delete event menu item to the event "..." menu, that publishes a deletion request to all available relays.
5. Always render a pretty OpenGraph card, for URLs, if they provide one. Unless the URL is in the middle of a list, paragraph, or otherwise part of some larger structure.
6. Make sure that highlights work, according to NIP-84. refer to ../jumble for a working version.
Some example events:
{
"id": "93bea17f71ed9ea7f6832e3be7e617b3387e0700193cfcebaf3ffbc2e6f48a7f",
"pubkey": "17538dc2a62769d09443f18c37cbe358fab5bbf981173542aa7c5ff171ed77c4",
"created_at": 1769023343,
"kind": 9802,
"tags": [
[
"e",
"6f854ade40cf3f24046249e650f55b33add3ee1526c00cc93cc7dfc80b8dc121",
"source"
]
],
"content": "not real wisdom, being a pretense of knowing the unknown",
"sig": "150279e733e16fa85439916f9f5b8108898a35cbf18062638dfc94e7a38f4a2faae8ce918750ef327fc16b7e7ca8739b1e8aff3b9dd238363d08eec423abba83"
}
{
"id": "1cd2017dd33a2efddffb9814c1993cf62e6d8a8e2e90af40973b6d4d1ea509f0",
"pubkey": "a9434ee165ed01b286becfc2771ef1705d3537d051b387288898cc00d5c885be",
"created_at": 1769288219,
"kind": 9802,
"tags": [
[
"p",
"a9434ee165ed01b286becfc2771ef1705d3537d051b387288898cc00d5c885be"
],
[
"a",
"30023:a9434ee165ed01b286becfc2771ef1705d3537d051b387288898cc00d5c885be:comparing-community-specs"
],
[
"context",
"A single publication can be targeted to up to 12 communities via one Targeted Publication event. The creator's intended audience is explicit and transparent — anyone can see which communities a piece of content was meant for. This can serve as an organic disovery route for related Communities + lowers the bar for bootstrapping new ones."
],
[
"alt",
"This highlight was made by https://primal.net web client"
]
],
"content": " The creator's intended audience is explicit and transparent — anyone can see which communities a piece of content was meant for. This can serve as an organic disovery route for related Communities + lowers the bar for bootstrapping new ones.",
"sig": "b490a12fbc1ab0063c6ddb3ae091212a4fcf76fdf9581d5f0291f24a9443b45d9f11d70e8035ea9c61b95ad47952c46ceeffa6dbb0fa5351bc51aad2e3d54add"
}
In the first highlight event, there is simply the content field, which should be rendered as a quote, with a link to the original source (event or URL) below it. If the URL provides OpenGraph data, display it and add the hyperlink to it. For events: display a card with "A note from: <user badge rendered>" and then the "title", "image", and "summary" tags, if available. Make the card a clickable hyperlink to the event's /event page.
7. Make #hashtags and t-tag topic buttons clickable. Clicking on one should launch a /topics/nameOfHashtag page, that reveals an event list of everything on the relays that includes that topic as a hashtag or a t-tag.
8. Display a metadata card, at the top of the page, when rendering any replaceable event in /event . Render tags like "image", "description", "summary", "author", "title", etc.
9. Add an Asciidoctor library to the packages. Use that for rendering kinds 30818 and 30041. All other kinds use Markdown.
10. If a /event page is opened for a 30040 event, make sure that you analyze and then lazy-load the entire event-index hierarchy (see ../nips-silberengel/NKBIP-01.adoc) into the cache and then into the view. The index can use a-tags or e-tags, or a mix of both. Handle both types of tags and make sure to render the events in the original order. Retry any missing events, after the first loading pass, but don't loop infinitely.
11. Display a metadata card, at the top, for the OP 30040. Only display metadata for nested events, if they differ from the OP.
12. Please note that kind 30040 events typically contain 30041s, but they can actually contain any type of event the creator wants. Make sure to render each one according to its kind (markdown or asciidoc).
13. Both the metadata card and the section events should have their "title" displayed (if none is provided, render the d-tag without hyphens and in Title Case) and have a "..." menu. The section events should have a new menu item: "Open in a new window" that opens the section as a /event in the browser. The index OP should have a new menu item: "Label this as a book" that creates a "general" 1985 label with "booklist".
14. If an event opened in /event has been highlighted, render the highlight on the displayed text. (for 30040s, this needs to run after the publication has finished loading, or it won't find the text). Hovering over the highlight should display the user-badge of the person who created the highlight, with a button "View the highlight". Clicking the button should make the highlight open to the right, in a thread panel.
15. There should be a /replaceable/d-tag-placed-here url path that searches for all replaceable events that have that d-tag and lists them in a list. Clicking one should display it in thread-panel on the right.
16. Add a main menu item, to the right of Feeds: Write
it should open to a page offering two choicees: find an existing event to edit, create a new event
Clicking find should then demand they enter an event id (hex id, nevent, naddr, note) and click "Find".
The event should be searched for in cache and then the relays, (return the newest version found) and the json rendered, below a hyperlink to the related /event page.
They should be able to click an "Edit" button, and then the event is displayed as a form, where they can add/edit/delete tags and change the content. Don't render id, kind, pubkey, sig, created_at as those are to be generated when they click "Publish". Publish to cache and to the standard write-relays. Publishing should reveal the standard success/failure message for the relays. If none were successful, allow them to attempt to republish from cache. If successful, wait 5 seconds and then, open the event in the /event page.
Clicking create should ask them to enter a kind they would like to write: 1, 11, 9802, 1222, 20, 21, 22, 30023, 30818, 30817, 30041, 30040 (metadata-only, no sections added, they can do that manually in the edit function, add that as a help-text), 1068
17. If the user is looking at their own profile page, display a menu item "Adjust profile events" that opens a left-side panel that allows them select one of the following events to create/update: 0, 3, 30315, 10133, 10002, 10432, 10001, 10003, 10895, 10015, 10030, 30030, 10000, 30008. Selecting one should open an appropriate form and preload it with any event found in cache or on the relays. Publish to cache and to the standard write-relays. Publishing should reveal the standard success/failure message for the relays. If none were successful, allow them to attempt to republish from cache. If successful, wait 5 seconds and then, open the event in the /event page.
18. Make sure the /event page can handle metadata-only (no "content") events gracefully, displaying their tag-lists.

4
public/healthz.json

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
"status": "ok",
"service": "aitherboard",
"version": "0.1.0",
"buildTime": "2026-02-04T04:42:40.028Z",
"buildTime": "2026-02-04T07:23:20.145Z",
"gitCommit": "unknown",
"timestamp": 1770180160028
"timestamp": 1770189800145
}

20
src/lib/components/EventMenu.svelte

@ -14,6 +14,9 @@ @@ -14,6 +14,9 @@
toggleHighlight
} from '../services/user-actions.js';
import { eventMenuStore } from '../services/event-menu-store.js';
import { sessionManager } from '../services/auth/session-manager.js';
import RelatedEventsModal from './modals/RelatedEventsModal.svelte';
import { KIND } from '../types/kind-lookup.js';
interface Props {
event: NostrEvent;
@ -24,6 +27,7 @@ @@ -24,6 +27,7 @@
let menuOpen = $state(false);
let jsonModalOpen = $state(false);
let relatedEventsModalOpen = $state(false);
let publicationModalOpen = $state(false);
let publicationResults = $state<{ success: string[]; failed: Array<{ relay: string; error: string }> } | null>(null);
let broadcasting = $state(false);
@ -36,7 +40,10 @@ @@ -36,7 +40,10 @@
let menuId = $derived(event.id);
// Check if this is a note with content (kind 1 or kind 11)
let isContentNote = $derived(event.kind === 1 || event.kind === 11);
let isContentNote = $derived(event.kind === KIND.SHORT_TEXT_NOTE || event.kind === KIND.DISCUSSION_THREAD);
// Check if user is logged in
let isLoggedIn = $derived(sessionManager.isLoggedIn());
// Track pin/bookmark/highlight state
let pinnedState = $state(false);
@ -172,6 +179,11 @@ @@ -172,6 +179,11 @@
closeMenu();
}
function viewRelatedEvents() {
relatedEventsModalOpen = true;
closeMenu();
}
async function broadcastEvent() {
broadcasting = true;
closeMenu();
@ -260,6 +272,11 @@ @@ -260,6 +272,11 @@
<button class="menu-item" onclick={viewJson}>
View JSON
</button>
{#if isLoggedIn}
<button class="menu-item" onclick={viewRelatedEvents}>
View your related events
</button>
{/if}
<button class="menu-item" onclick={broadcastEvent} disabled={broadcasting}>
{broadcasting ? 'Broadcasting...' : 'Broadcast event'}
</button>
@ -296,6 +313,7 @@ @@ -296,6 +313,7 @@
</div>
<EventJsonModal bind:open={jsonModalOpen} event={event} />
<RelatedEventsModal bind:open={relatedEventsModalOpen} event={event} />
<PublicationStatusModal bind:open={publicationModalOpen} bind:results={publicationResults} />
<style>

2
src/lib/components/content/EmbeddedEvent.svelte

@ -117,7 +117,7 @@ @@ -117,7 +117,7 @@
function getTitle(): string {
if (!event) return '';
if (event.kind === 11) {
if (event.kind === KIND.DISCUSSION_THREAD) {
const titleTag = event.tags.find(t => t[0] === 'title');
return titleTag?.[1] || '';
}

2
src/lib/components/content/QuotedContext.svelte

@ -41,7 +41,7 @@ @@ -41,7 +41,7 @@
try {
const relays = relayManager.getFeedReadRelays();
const events = await nostrClient.fetchEvents(
[{ kinds: [1], ids: [eventId] }],
[{ kinds: [KIND.SHORT_TEXT_NOTE], ids: [eventId] }],
relays,
{ useCache: true, cacheResults: true }
);

2
src/lib/components/content/ReplyContext.svelte

@ -41,7 +41,7 @@ @@ -41,7 +41,7 @@
try {
const relays = relayManager.getFeedReadRelays();
const events = await nostrClient.fetchEvents(
[{ kinds: [1], ids: [eventId] }],
[{ kinds: [KIND.SHORT_TEXT_NOTE], ids: [eventId] }],
relays,
{ useCache: true, cacheResults: true }
);

17
src/lib/components/modals/EventJsonModal.svelte

@ -38,13 +38,24 @@ @@ -38,13 +38,24 @@
{#if open && event}
<div
class="modal-overlay"
onclick={close}
onkeydown={(e) => e.key === 'Escape' && close()}
onclick={(e) => {
// Only close if clicking directly on the overlay, not on modal content
if (e.target === e.currentTarget) {
close();
}
}}
onkeydown={(e) => {
if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
close();
}
}}
role="dialog"
aria-modal="true"
aria-label="Event JSON modal"
tabindex="-1"
>
<div class="modal-content" onclick={(e) => e.stopPropagation()}>
<div class="modal-content">
<div class="modal-header">
<h2>Event JSON</h2>
<button onclick={close} class="close-button">×</button>

352
src/lib/components/modals/RelatedEventsModal.svelte

@ -0,0 +1,352 @@ @@ -0,0 +1,352 @@
<script lang="ts">
import type { NostrEvent } from '../../types/nostr.js';
import { nostrClient } from '../../services/nostr/nostr-client.js';
import { relayManager } from '../../services/nostr/relay-manager.js';
import { sessionManager } from '../../services/auth/session-manager.js';
import { onMount } from 'svelte';
interface Props {
open?: boolean;
event?: NostrEvent | null;
}
let { open = $bindable(false), event = $bindable(null) }: Props = $props();
let relatedEvents = $state<NostrEvent[]>([]);
let loading = $state(false);
let error = $state<string | null>(null);
let jsonText = $derived(JSON.stringify(relatedEvents, null, 2));
let copied = $state(false);
// Get current user's pubkey
const currentPubkey = $derived(sessionManager.getCurrentPubkey());
// Get replaceable event address (kind:pubkey:d-tag) if event is replaceable
function getReplaceableAddress(event: NostrEvent): string | null {
// Replaceable events have a 'd' tag
const dTag = event.tags.find(t => t[0] === 'd' || t[0] === 'D');
if (dTag && dTag[1]) {
return `${event.kind}:${event.pubkey}:${dTag[1]}`;
}
return null;
}
async function loadRelatedEvents() {
if (!event || !currentPubkey) {
error = 'No event or user not logged in';
return;
}
loading = true;
error = null;
relatedEvents = [];
try {
const relays = relayManager.getProfileReadRelays();
const filters: any[] = [];
// Query for events with #e tag (event reference)
filters.push({
authors: [currentPubkey],
'#e': [event.id],
limit: 100
});
// Query for events with #q tag (quoted event)
filters.push({
authors: [currentPubkey],
'#q': [event.id],
limit: 100
});
// Query for events with #a tag (replaceable event address) - only if event is replaceable
const replaceableAddress = getReplaceableAddress(event);
if (replaceableAddress) {
filters.push({
authors: [currentPubkey],
'#a': [replaceableAddress],
limit: 100
});
}
// Fetch all related events
const allEvents = await nostrClient.fetchEvents(
filters,
relays,
{ useCache: true, cacheResults: true, timeout: 10000 }
);
// Deduplicate by event ID
const uniqueEvents = Array.from(
new Map(allEvents.map(e => [e.id, e])).values()
);
// Sort by created_at descending
relatedEvents = uniqueEvents.sort((a, b) => b.created_at - a.created_at);
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to load related events';
console.error('Error loading related events:', err);
} finally {
loading = false;
}
}
function close() {
open = false;
}
async function copyJson() {
if (!jsonText) return;
try {
await navigator.clipboard.writeText(jsonText);
copied = true;
setTimeout(() => {
copied = false;
}, 2000);
} catch (error) {
console.error('Failed to copy JSON:', error);
}
}
function selectAll() {
const textarea = document.querySelector('.json-textarea') as HTMLTextAreaElement;
if (textarea) {
textarea.select();
}
}
// Load related events when modal opens
$effect(() => {
if (open && event && currentPubkey) {
loadRelatedEvents();
} else if (!open) {
// Reset state when closing
relatedEvents = [];
error = null;
}
});
</script>
{#if open && event}
<div
class="modal-overlay"
onclick={(e) => {
// Only close if clicking directly on the overlay, not on modal content
if (e.target === e.currentTarget) {
close();
}
}}
onkeydown={(e) => {
if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
close();
}
}}
role="dialog"
aria-modal="true"
aria-label="Related events modal"
tabindex="-1"
>
<div class="modal-content">
<div class="modal-header">
<h2>Your Related Events</h2>
<button onclick={close} class="close-button">×</button>
</div>
<div class="modal-body">
{#if loading}
<div class="loading-state">
<p class="text-fog-text-light dark:text-fog-dark-text-light">Loading related events...</p>
</div>
{:else if error}
<div class="error-state">
<p class="text-red-600 dark:text-red-400">Error: {error}</p>
</div>
{:else if relatedEvents.length === 0}
<div class="empty-state">
<p class="text-fog-text-light dark:text-fog-dark-text-light">No related events found.</p>
<p class="text-fog-text-light dark:text-fog-dark-text-light text-sm mt-2">
This shows events you've signed that reference this event via 'e', 'q', or 'a' tags.
</p>
</div>
{:else}
<div class="events-info">
<p class="text-fog-text-light dark:text-fog-dark-text-light text-sm mb-2">
Found {relatedEvents.length} related event{relatedEvents.length !== 1 ? 's' : ''}:
</p>
</div>
<textarea
class="json-textarea"
readonly
value={jsonText}
onclick={selectAll}
></textarea>
{/if}
</div>
<div class="modal-footer">
{#if relatedEvents.length > 0}
<button onclick={copyJson} class="copy-button">
{copied ? 'Copied!' : 'Copy'}
</button>
{/if}
<button onclick={close}>Close</button>
</div>
</div>
</div>
{/if}
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.4);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: var(--fog-post, #ffffff);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 8px;
max-width: 900px;
width: 90%;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
:global(.dark) .modal-content {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .modal-header {
border-bottom-color: var(--fog-dark-border, #374151);
}
.modal-header h2 {
margin: 0;
font-size: 1.25rem;
color: var(--fog-text, #1f2937);
}
:global(.dark) .modal-header h2 {
color: var(--fog-dark-text, #f1f5f9);
}
.close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 2rem;
height: 2rem;
color: var(--fog-text, #1f2937);
}
:global(.dark) .close-button {
color: var(--fog-dark-text, #f1f5f9);
}
.modal-body {
padding: 1rem;
flex: 1;
overflow: auto;
}
.loading-state,
.empty-state,
.error-state {
padding: 2rem;
text-align: center;
}
.events-info {
margin-bottom: 0.5rem;
}
.json-textarea {
width: 100%;
min-height: 400px;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
padding: 0.75rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 4px;
background: var(--fog-bg, #ffffff);
color: var(--fog-text, #1f2937);
resize: vertical;
}
:global(.dark) .json-textarea {
background: var(--fog-dark-bg, #0f172a);
border-color: var(--fog-dark-border, #374151);
color: var(--fog-dark-text, #f1f5f9);
}
.modal-footer {
padding: 1rem;
border-top: 1px solid var(--fog-border, #e5e7eb);
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
:global(.dark) .modal-footer {
border-top-color: var(--fog-dark-border, #374151);
}
.modal-footer button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
font-size: 0.875rem;
}
.copy-button {
background: var(--fog-accent, #64748b);
color: white;
}
.copy-button:hover {
background: var(--fog-accent-dark, #475569);
}
.modal-footer button:not(.copy-button) {
background: var(--fog-border, #e5e7eb);
color: var(--fog-text, #1f2937);
}
.modal-footer button:not(.copy-button):hover {
background: var(--fog-highlight, #f3f4f6);
}
:global(.dark) .modal-footer button:not(.copy-button) {
background: var(--fog-dark-border, #374151);
color: var(--fog-dark-text, #f1f5f9);
}
:global(.dark) .modal-footer button:not(.copy-button):hover {
background: var(--fog-dark-highlight, #374151);
}
</style>

8
src/lib/modules/comments/CommentForm.svelte

@ -44,17 +44,17 @@ @@ -44,17 +44,17 @@
// If replying to a parent event, check its kind
if (parentEvent) {
// If parent is kind 1, reply with kind 1
if (parentEvent.kind === 1) return 1;
if (parentEvent.kind === KIND.SHORT_TEXT_NOTE) return KIND.SHORT_TEXT_NOTE;
// Everything else gets kind 1111
return 1111;
return KIND.COMMENT;
}
// If replying to root, check root kind
if (rootEvent) {
// If root is kind 1, reply with kind 1
if (rootEvent.kind === 1) return 1;
if (rootEvent.kind === KIND.SHORT_TEXT_NOTE) return KIND.SHORT_TEXT_NOTE;
// Everything else gets kind 1111
return 1111;
return KIND.COMMENT;
}
// Default to kind 1111 if we can't determine

103
src/lib/modules/comments/CommentThread.svelte

@ -7,6 +7,7 @@ @@ -7,6 +7,7 @@
import { relayManager } from '../../services/nostr/relay-manager.js';
import { onMount } from 'svelte';
import type { NostrEvent } from '../../types/nostr.js';
import { KIND } from '../../types/kind-lookup.js';
interface Props {
threadId: string; // The event ID of the root event
@ -25,7 +26,7 @@ @@ -25,7 +26,7 @@
let nestedSubscriptionActive = $state(false); // Track if nested subscription is active
let isProcessingUpdate = $state(false); // Prevent recursive update processing
const isKind1 = $derived(event?.kind === 1);
const isKind1 = $derived(event?.kind === KIND.SHORT_TEXT_NOTE);
const rootKind = $derived(event?.kind || null);
onMount(async () => {
@ -76,7 +77,7 @@ @@ -76,7 +77,7 @@
*/
function getParentEventId(replyEvent: NostrEvent): string | null {
// For kind 1111, check both uppercase and lowercase E and A tags
if (replyEvent.kind === 1111) {
if (replyEvent.kind === KIND.COMMENT) {
// Check uppercase E tag first (NIP-22 standard for root)
const eTag = replyEvent.tags.find((t) => t[0] === 'E');
if (eTag && eTag[1]) {
@ -112,7 +113,7 @@ @@ -112,7 +113,7 @@
}
// For kind 1, 1244, 9735: check e tag
if (replyEvent.kind === 1 || replyEvent.kind === 1244 || replyEvent.kind === 9735) {
if (replyEvent.kind === KIND.SHORT_TEXT_NOTE || replyEvent.kind === KIND.VOICE_REPLY || replyEvent.kind === KIND.ZAP_RECEIPT) {
// For kind 1, check all e tags (NIP-10)
const eTags = replyEvent.tags.filter((t) => t[0] === 'e' && t[1] && t[1] !== replyEvent.id);
// Prefer e tag with 'reply' marker, otherwise use first e tag
@ -130,7 +131,7 @@ @@ -130,7 +131,7 @@
* For other kinds: checks e tag
*/
function referencesRoot(replyEvent: NostrEvent): boolean {
if (replyEvent.kind === 1111) {
if (replyEvent.kind === KIND.COMMENT) {
// Check uppercase E tag (NIP-22 standard for root)
const eTag = replyEvent.tags.find((t) => t[0] === 'E');
if (eTag && eTag[1] === threadId) return true;
@ -201,16 +202,16 @@ @@ -201,16 +202,16 @@
}
// Add the reply to the appropriate map
if (reply.kind === 1111) {
if (reply.kind === KIND.COMMENT) {
commentsMap.set(reply.id, reply);
hasNewReplies = true;
} else if (reply.kind === 1) {
} else if (reply.kind === KIND.SHORT_TEXT_NOTE) {
kind1RepliesMap.set(reply.id, reply);
hasNewReplies = true;
} else if (reply.kind === 1244) {
} else if (reply.kind === KIND.VOICE_REPLY) {
yakBacksMap.set(reply.id, reply);
hasNewReplies = true;
} else if (reply.kind === 9735) {
} else if (reply.kind === KIND.ZAP_RECEIPT) {
zapReceiptsMap.set(reply.id, reply);
hasNewReplies = true;
}
@ -236,14 +237,14 @@ @@ -236,14 +237,14 @@
}
const allRelays = relayManager.getProfileReadRelays();
const replyFilters: any[] = [
{ kinds: [1111], '#e': [threadId] },
{ kinds: [1111], '#E': [threadId] },
{ kinds: [1111], '#a': [threadId] },
{ kinds: [1111], '#A': [threadId] },
{ kinds: [1], '#e': [threadId] },
{ kinds: [1244], '#e': [threadId] },
{ kinds: [9735], '#e': [threadId] }
const replyFilters: any[] = [
{ kinds: [KIND.COMMENT], '#e': [threadId] },
{ kinds: [KIND.COMMENT], '#E': [threadId] },
{ kinds: [KIND.COMMENT], '#a': [threadId] },
{ kinds: [KIND.COMMENT], '#A': [threadId] },
{ kinds: [KIND.SHORT_TEXT_NOTE], '#e': [threadId] },
{ kinds: [KIND.VOICE_REPLY], '#e': [threadId] },
{ kinds: [KIND.ZAP_RECEIPT], '#e': [threadId] }
];
// fetchEvents with useCache:true returns cached data immediately if available,
@ -276,10 +277,10 @@ @@ -276,10 +277,10 @@
const rootReplies = allReplies.filter(reply => referencesRoot(reply));
// Separate by type
comments = rootReplies.filter(e => e.kind === 1111);
kind1Replies = rootReplies.filter(e => e.kind === 1);
yakBacks = rootReplies.filter(e => e.kind === 1244);
zapReceipts = rootReplies.filter(e => e.kind === 9735);
comments = rootReplies.filter(e => e.kind === KIND.COMMENT);
kind1Replies = rootReplies.filter(e => e.kind === KIND.SHORT_TEXT_NOTE);
yakBacks = rootReplies.filter(e => e.kind === KIND.VOICE_REPLY);
zapReceipts = rootReplies.filter(e => e.kind === KIND.ZAP_RECEIPT);
loading = false; // Hide loading now that we have data (cached or fresh)
@ -320,11 +321,11 @@ @@ -320,11 +321,11 @@
// Use a single subscription that covers all reply IDs
const nestedFilters: any[] = [
{ kinds: [1111], '#e': Array.from(allReplyIds) },
{ kinds: [1111], '#E': Array.from(allReplyIds) },
{ kinds: [1], '#e': Array.from(allReplyIds) },
{ kinds: [1244], '#e': Array.from(allReplyIds) },
{ kinds: [9735], '#e': Array.from(allReplyIds) }
{ kinds: [KIND.COMMENT], '#e': Array.from(allReplyIds) },
{ kinds: [KIND.COMMENT], '#E': Array.from(allReplyIds) },
{ kinds: [KIND.SHORT_TEXT_NOTE], '#e': Array.from(allReplyIds) },
{ kinds: [KIND.VOICE_REPLY], '#e': Array.from(allReplyIds) },
{ kinds: [KIND.ZAP_RECEIPT], '#e': Array.from(allReplyIds) }
];
nostrClient.fetchEvents(
@ -361,16 +362,16 @@ @@ -361,16 +362,16 @@
if (allReplyIds.size > 0) {
const nestedFilters: any[] = [
// Fetch nested kind 1111 comments - check both e/E and a/A tags
{ kinds: [1111], '#e': Array.from(allReplyIds) },
{ kinds: [1111], '#E': Array.from(allReplyIds) },
{ kinds: [1111], '#a': Array.from(allReplyIds) },
{ kinds: [1111], '#A': Array.from(allReplyIds) },
{ kinds: [KIND.COMMENT], '#e': Array.from(allReplyIds) },
{ kinds: [KIND.COMMENT], '#E': Array.from(allReplyIds) },
{ kinds: [KIND.COMMENT], '#a': Array.from(allReplyIds) },
{ kinds: [KIND.COMMENT], '#A': Array.from(allReplyIds) },
// Fetch nested kind 1 replies
{ kinds: [1], '#e': Array.from(allReplyIds) },
{ kinds: [KIND.SHORT_TEXT_NOTE], '#e': Array.from(allReplyIds) },
// Fetch nested yak backs
{ kinds: [1244], '#e': Array.from(allReplyIds) },
{ kinds: [KIND.VOICE_REPLY], '#e': Array.from(allReplyIds) },
// Fetch nested zap receipts
{ kinds: [9735], '#e': Array.from(allReplyIds) }
{ kinds: [KIND.ZAP_RECEIPT], '#e': Array.from(allReplyIds) }
];
const nestedReplies = await nostrClient.fetchEvents(
@ -381,16 +382,16 @@ @@ -381,16 +382,16 @@
// Add new replies by type
for (const reply of nestedReplies) {
if (reply.kind === 1111 && !comments.some(c => c.id === reply.id)) {
if (reply.kind === KIND.COMMENT && !comments.some(c => c.id === reply.id)) {
comments.push(reply);
hasNewReplies = true;
} else if (reply.kind === 1 && !kind1Replies.some(r => r.id === reply.id)) {
} else if (reply.kind === KIND.SHORT_TEXT_NOTE && !kind1Replies.some(r => r.id === reply.id)) {
kind1Replies.push(reply);
hasNewReplies = true;
} else if (reply.kind === 1244 && !yakBacks.some(y => y.id === reply.id)) {
} else if (reply.kind === KIND.VOICE_REPLY && !yakBacks.some(y => y.id === reply.id)) {
yakBacks.push(reply);
hasNewReplies = true;
} else if (reply.kind === 9735 && !zapReceipts.some(z => z.id === reply.id)) {
} else if (reply.kind === KIND.ZAP_RECEIPT && !zapReceipts.some(z => z.id === reply.id)) {
zapReceipts.push(reply);
hasNewReplies = true;
}
@ -536,21 +537,21 @@ @@ -536,21 +537,21 @@
// Always fetch kind 1111 comments - check both e and E tags, and a and A tags
replyFilters.push(
{ kinds: [1111], '#e': [threadId] }, // Lowercase e tag
{ kinds: [1111], '#E': [threadId] }, // Uppercase E tag (NIP-22)
{ kinds: [1111], '#a': [threadId] }, // Lowercase a tag (some clients use wrong tags)
{ kinds: [1111], '#A': [threadId] } // Uppercase A tag (NIP-22 for addressable events)
{ kinds: [KIND.COMMENT], '#e': [threadId] }, // Lowercase e tag
{ kinds: [KIND.COMMENT], '#E': [threadId] }, // Uppercase E tag (NIP-22)
{ kinds: [KIND.COMMENT], '#a': [threadId] }, // Lowercase a tag (some clients use wrong tags)
{ kinds: [KIND.COMMENT], '#A': [threadId] } // Uppercase A tag (NIP-22 for addressable events)
);
// For kind 1 events, fetch kind 1 replies
// Also fetch kind 1 replies for any event (some apps use kind 1 for everything)
replyFilters.push({ kinds: [1], '#e': [threadId] });
replyFilters.push({ kinds: [KIND.SHORT_TEXT_NOTE], '#e': [threadId] });
// Fetch yak backs (kind 1244) - voice replies
replyFilters.push({ kinds: [1244], '#e': [threadId] });
replyFilters.push({ kinds: [KIND.VOICE_REPLY], '#e': [threadId] });
// Fetch zap receipts (kind 9735)
replyFilters.push({ kinds: [9735], '#e': [threadId] });
replyFilters.push({ kinds: [KIND.ZAP_RECEIPT], '#e': [threadId] });
// Don't use cache when reloading after publishing - we want fresh data
const allReplies = await nostrClient.fetchEvents(
@ -563,10 +564,10 @@ @@ -563,10 +564,10 @@
const rootReplies = allReplies.filter(reply => referencesRoot(reply));
// Separate by type
comments = rootReplies.filter(e => e.kind === 1111);
kind1Replies = rootReplies.filter(e => e.kind === 1);
yakBacks = rootReplies.filter(e => e.kind === 1244);
zapReceipts = rootReplies.filter(e => e.kind === 9735);
comments = rootReplies.filter(e => e.kind === KIND.COMMENT);
kind1Replies = rootReplies.filter(e => e.kind === KIND.SHORT_TEXT_NOTE);
yakBacks = rootReplies.filter(e => e.kind === KIND.VOICE_REPLY);
zapReceipts = rootReplies.filter(e => e.kind === KIND.ZAP_RECEIPT);
// Recursively fetch all nested replies (non-blocking - let it run in background)
fetchNestedReplies().then(() => {
@ -592,15 +593,15 @@ @@ -592,15 +593,15 @@
function getAllowedReplyKind(targetEvent: NostrEvent | null): number {
if (!targetEvent) {
// If replying to root, check root kind
if (isKind1) return 1;
return 1111;
if (isKind1) return KIND.SHORT_TEXT_NOTE;
return KIND.COMMENT;
}
// If target is kind 1, allow kind 1 reply
if (targetEvent.kind === 1) return 1;
if (targetEvent.kind === KIND.SHORT_TEXT_NOTE) return KIND.SHORT_TEXT_NOTE;
// Everything else gets kind 1111
return 1111;
return KIND.COMMENT;
}
// Calculate total comment count (includes all reply types)

13
src/lib/modules/feed/FeedPage.svelte

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
import ThreadDrawer from './ThreadDrawer.svelte';
import type { NostrEvent } from '../../types/nostr.js';
import { onMount, tick } from 'svelte';
import { KIND } from '../../types/kind-lookup.js';
let posts = $state<NostrEvent[]>([]);
let loading = $state(true);
@ -108,7 +109,7 @@ @@ -108,7 +109,7 @@
}
const relays = relayManager.getFeedReadRelays();
const filters = [{ kinds: [1], limit: 20 }];
const filters = [{ kinds: [KIND.SHORT_TEXT_NOTE], limit: 20 }];
// Subscribe to new kind 1 events
subscriptionId = nostrClient.subscribe(
@ -146,7 +147,7 @@ @@ -146,7 +147,7 @@
: Math.floor(Date.now() / 1000) - 60; // Last minute if no posts
const filters = [{
kinds: [1],
kinds: [KIND.SHORT_TEXT_NOTE],
limit: 50,
since: newestTimestamp + 1 // Only get events newer than what we have
}];
@ -205,7 +206,7 @@ @@ -205,7 +206,7 @@
const relays = relayManager.getFeedReadRelays();
// Load initial feed - use cache for fast initial load
const filters = [{ kinds: [1], limit: 20 }];
const filters = [{ kinds: [KIND.SHORT_TEXT_NOTE], limit: 20 }];
const events = await nostrClient.fetchEvents(
filters,
relays,
@ -275,7 +276,7 @@ @@ -275,7 +276,7 @@
const relays = relayManager.getFeedReadRelays();
const filters = [{
kinds: [1],
kinds: [KIND.SHORT_TEXT_NOTE],
limit: 20,
until: oldestTimestamp || undefined
}];
@ -407,8 +408,8 @@ @@ -407,8 +408,8 @@
// Batch fetch all reactions for all posts in one query
const allReactions = await nostrClient.fetchEvents(
[
{ kinds: [7], '#e': eventIds, limit: 1000 },
{ kinds: [7], '#E': eventIds, limit: 1000 }
{ kinds: [KIND.REACTION], '#e': eventIds, limit: 1000 },
{ kinds: [KIND.REACTION], '#E': eventIds, limit: 1000 }
],
reactionRelays,
{ useCache: true, cacheResults: true, timeout: 10000 }

18
src/lib/modules/feed/FeedPost.svelte

@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
import { relayManager } from '../../services/nostr/relay-manager.js';
import { onMount } from 'svelte';
import type { NostrEvent } from '../../types/nostr.js';
import { getKindInfo } from '../../types/kind-lookup.js';
import { getKindInfo, KIND } from '../../types/kind-lookup.js';
import { stripMarkdown } from '../../services/text-utils.js';
interface Props {
@ -37,7 +37,7 @@ @@ -37,7 +37,7 @@
// Calculate votes as derived values to avoid infinite loops
// Deduplicate by pubkey - each user should only count once per vote type
let upvotes = $derived.by(() => {
if (post.kind !== 11) return 0;
if (post.kind !== KIND.DISCUSSION_THREAD) return 0;
const reactionEvents = reactions;
if (!reactionEvents || !Array.isArray(reactionEvents)) return 0;
@ -73,7 +73,7 @@ @@ -73,7 +73,7 @@
});
let downvotes = $derived.by(() => {
if (post.kind !== 11) return 0;
if (post.kind !== KIND.DISCUSSION_THREAD) return 0;
const reactionEvents = reactions;
if (!reactionEvents || !Array.isArray(reactionEvents)) return 0;
@ -120,7 +120,7 @@ @@ -120,7 +120,7 @@
const zapRelays = relayManager.getZapReceiptReadRelays();
const filters = [{
kinds: [9735],
kinds: [KIND.ZAP_RECEIPT],
'#e': [post.id]
}];
@ -238,7 +238,7 @@ @@ -238,7 +238,7 @@
try {
const relays = relayManager.getFeedReadRelays();
const events = await nostrClient.fetchEvents(
[{ kinds: [1], ids: [replyEventId] }],
[{ kinds: [KIND.SHORT_TEXT_NOTE], ids: [replyEventId] }],
relays,
{ useCache: true, cacheResults: true }
);
@ -372,7 +372,7 @@ @@ -372,7 +372,7 @@
{#if getClientName()}
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">via {getClientName()}</span>
{/if}
{#if post.kind === 11}
{#if post.kind === KIND.DISCUSSION_THREAD}
{@const topics = getTopics()}
{#if topics.length === 0}
<span class="topic-badge text-xs px-2 py-0.5 rounded bg-fog-border dark:bg-fog-dark-border text-fog-text-light dark:text-fog-dark-text-light">General</span>
@ -388,7 +388,7 @@ @@ -388,7 +388,7 @@
<div class="flex items-center justify-between text-xs text-fog-text dark:text-fog-dark-text">
<div class="flex items-center gap-2">
{#if post.kind === 11 && (upvotes > 0 || downvotes > 0)}
{#if post.kind === KIND.DISCUSSION_THREAD && (upvotes > 0 || downvotes > 0)}
<span class="vote-counts text-fog-text-light dark:text-fog-dark-text-light">
{#if upvotes > 0}
<span class="upvotes"> {upvotes}</span>
@ -431,7 +431,7 @@ @@ -431,7 +431,7 @@
{#if getClientName()}
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light flex-shrink-0">via {getClientName()}</span>
{/if}
{#if post.kind === 11}
{#if post.kind === KIND.DISCUSSION_THREAD}
{@const topics = post.tags.filter((t) => t[0] === 't').map((t) => t[1])}
{#if topics.length === 0}
<span class="topic-badge text-xs px-2 py-0.5 rounded bg-fog-border dark:bg-fog-dark-border text-fog-text-light dark:text-fog-dark-text-light">General</span>
@ -463,7 +463,7 @@ @@ -463,7 +463,7 @@
<!-- Post actions (reactions, etc.) - always visible, outside collapsible content -->
<div class="post-actions flex flex-wrap items-center gap-2 sm:gap-4">
{#if post.kind === 11}
{#if post.kind === KIND.DISCUSSION_THREAD}
<!-- Show vote counts for threads -->
{#if upvotes > 0 || downvotes > 0}
<span class="vote-counts text-xs text-fog-text-light dark:text-fog-dark-text-light">

2
src/lib/modules/feed/Reply.svelte

@ -92,7 +92,7 @@ @@ -92,7 +92,7 @@
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">via {getClientName()}</span>
{/if}
<div class="ml-auto">
<EventMenu event={reply} showContentActions={reply.kind === 1} />
<EventMenu event={reply} showContentActions={reply.kind === KIND.SHORT_TEXT_NOTE} />
</div>
</div>

2
src/lib/modules/profiles/PaymentAddresses.svelte

@ -27,7 +27,7 @@ @@ -27,7 +27,7 @@
// Fetch kind 10133 (payment targets)
const paymentEvents = await nostrClient.fetchEvents(
[{ kinds: [10133], authors: [pubkey], limit: 1 }],
[{ kinds: [KIND.PAYMENT_ADDRESSES], authors: [pubkey], limit: 1 }],
[...config.defaultRelays, ...config.profileRelays],
{ useCache: true, cacheResults: true }
);

11
src/lib/modules/profiles/ProfilePage.svelte

@ -12,6 +12,7 @@ @@ -12,6 +12,7 @@
import { page } from '$app/stores';
import { nip19 } from 'nostr-tools';
import type { NostrEvent } from '../../types/nostr.js';
import { KIND } from '../../types/kind-lookup.js';
let profile = $state<ProfileData | null>(null);
let userStatus = $state<string | null>(null);
@ -72,7 +73,7 @@ @@ -72,7 +73,7 @@
// Fetch current user's posts from cache first (fast)
const currentUserPosts = await nostrClient.fetchEvents(
[{ kinds: [1], authors: [currentUserPubkey], limit: 50 }],
[{ kinds: [KIND.SHORT_TEXT_NOTE], authors: [currentUserPubkey], limit: 50 }],
interactionRelays,
{ useCache: true, cacheResults: true, timeout: 2000 } // Short timeout for cache
);
@ -88,8 +89,8 @@ @@ -88,8 +89,8 @@
const interactionEvents = await Promise.race([
nostrClient.fetchEvents(
[
{ kinds: [1], authors: [profilePubkey], '#e': Array.from(currentUserPostIds).slice(0, 20), limit: 20 }, // Limit IDs to avoid huge queries
{ kinds: [1], authors: [profilePubkey], '#p': [currentUserPubkey], limit: 20 }
{ kinds: [KIND.SHORT_TEXT_NOTE], authors: [profilePubkey], '#e': Array.from(currentUserPostIds).slice(0, 20), limit: 20 }, // Limit IDs to avoid huge queries
{ kinds: [KIND.SHORT_TEXT_NOTE], authors: [profilePubkey], '#p': [currentUserPubkey], limit: 20 }
],
interactionRelays,
{ useCache: true, cacheResults: true, timeout: 5000 }
@ -349,7 +350,7 @@ @@ -349,7 +350,7 @@
// Load posts first (needed for response filtering)
const feedEvents = await nostrClient.fetchEvents(
[{ kinds: [1], authors: [pubkey], limit: 20 }],
[{ kinds: [KIND.SHORT_TEXT_NOTE], authors: [pubkey], limit: 20 }],
profileRelays,
{ useCache: true, cacheResults: true, timeout: 5000 }
);
@ -358,7 +359,7 @@ @@ -358,7 +359,7 @@
// Load responses in parallel with posts (but filter after posts are loaded)
const userPostIds = new Set(posts.map(p => p.id));
const responseEvents = await nostrClient.fetchEvents(
[{ kinds: [1], '#p': [pubkey], limit: 50 }], // Fetch more to account for filtering
[{ kinds: [KIND.SHORT_TEXT_NOTE], '#p': [pubkey], limit: 50 }], // Fetch more to account for filtering
responseRelays,
{ useCache: true, cacheResults: true, timeout: 5000 }
);

23
src/lib/modules/reactions/FeedReactionButtons.svelte

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
import { relayManager } from '../../services/nostr/relay-manager.js';
import { onMount } from 'svelte';
import type { NostrEvent } from '../../types/nostr.js';
import { KIND } from '../../types/kind-lookup.js';
import { resolveCustomEmojis, fetchEmojiSet, resolveEmojiShortcode } from '../../services/nostr/nip30-emoji.js';
import EmojiPicker from '../../components/content/EmojiPicker.svelte';
import emojiData from 'unicode-emoji-json/data-ordered-emoji.json';
@ -110,12 +111,12 @@ @@ -110,12 +111,12 @@
allReactionsMap.clear();
const reactionsWithLowerE = await nostrClient.fetchEvents(
[{ kinds: [7], '#e': [event.id] }],
[{ kinds: [KIND.REACTION], '#e': [event.id] }],
reactionRelays,
{ useCache: true, cacheResults: true, onUpdate: handleReactionUpdate }
);
const reactionsWithUpperE = await nostrClient.fetchEvents(
[{ kinds: [7], '#E': [event.id] }],
[{ kinds: [KIND.REACTION], '#E': [event.id] }],
reactionRelays,
{ useCache: true, cacheResults: true, onUpdate: handleReactionUpdate }
);
@ -191,7 +192,7 @@ @@ -191,7 +192,7 @@
// Fetch deletion events (kind 5) to filter out deleted reactions
const reactionRelays = relayManager.getProfileReadRelays();
const deletionEvents = await nostrClient.fetchEvents(
[{ kinds: [5], authors: Array.from(new Set(reactions.map(r => r.pubkey))) }],
[{ kinds: [KIND.EVENT_DELETION], authors: Array.from(new Set(reactions.map(r => r.pubkey))) }],
reactionRelays,
{ useCache: true }
);
@ -258,7 +259,7 @@ @@ -258,7 +259,7 @@
// For kind 11 events (or kind 1111 replies to kind 11), normalize reactions: only + and - allowed
// Backward compatibility: ⬆/↑ = +, ⬇/↓ = -
if (event.kind === 11 || forceUpvoteDownvote) {
if (event.kind === KIND.DISCUSSION_THREAD || forceUpvoteDownvote) {
if (content === '⬆' || content === '↑') {
content = '+';
} else if (content === '⬇' || content === '↓') {
@ -379,7 +380,7 @@ @@ -379,7 +380,7 @@
}
// For kind 11 events (or kind 1111 replies to kind 11), only allow + and - (upvote/downvote)
if ((event.kind === 11 || forceUpvoteDownvote) && content !== '+' && content !== '-') {
if ((event.kind === KIND.DISCUSSION_THREAD || forceUpvoteDownvote) && content !== '+' && content !== '-') {
return;
}
@ -389,7 +390,7 @@ @@ -389,7 +390,7 @@
if (userReactionEventId) {
try {
const deletionEvent: Omit<NostrEvent, 'id' | 'sig'> = {
kind: 5,
kind: KIND.EVENT_DELETION,
pubkey: sessionManager.getCurrentPubkey()!,
created_at: Math.floor(Date.now() / 1000),
tags: [['e', userReactionEventId]],
@ -437,12 +438,12 @@ @@ -437,12 +438,12 @@
}
// For kind 11 (or kind 1111 replies to kind 11): if user has the opposite vote, delete it first
if ((event.kind === 11 || forceUpvoteDownvote) && userReaction && userReaction !== content) {
if ((event.kind === KIND.DISCUSSION_THREAD || forceUpvoteDownvote) && userReaction && userReaction !== content) {
// Delete the existing vote first
if (userReactionEventId) {
try {
const deletionEvent: Omit<NostrEvent, 'id' | 'sig'> = {
kind: 5,
kind: KIND.EVENT_DELETION,
pubkey: sessionManager.getCurrentPubkey()!,
created_at: Math.floor(Date.now() / 1000),
tags: [['e', userReactionEventId]],
@ -485,7 +486,7 @@ @@ -485,7 +486,7 @@
}
const reactionEvent: Omit<NostrEvent, 'id' | 'sig'> = {
kind: 7,
kind: KIND.REACTION,
pubkey: sessionManager.getCurrentPubkey()!,
created_at: Math.floor(Date.now() / 1000),
tags,
@ -635,7 +636,7 @@ @@ -635,7 +636,7 @@
</script>
<div class="Feed-reaction-buttons flex gap-2 items-center flex-wrap">
{#if event.kind === 11 || forceUpvoteDownvote}
{#if event.kind === KIND.DISCUSSION_THREAD || forceUpvoteDownvote}
<!-- Kind 11 (Thread) or Kind 1111 (Reply to Thread): Only upvote and downvote buttons -->
<button
onclick={() => toggleReaction('+')}
@ -676,7 +677,7 @@ @@ -676,7 +677,7 @@
/>
</div>
{#if event.kind !== 11}
{#if event.kind !== KIND.DISCUSSION_THREAD}
{#each getAllReactions() as { content, count }}
<span
class="reaction-display {userReaction === content ? 'active' : ''}"

11
src/lib/modules/reactions/ReactionButtons.svelte

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
import { nostrClient } from '../../services/nostr/nostr-client.js';
import { onMount } from 'svelte';
import type { NostrEvent } from '../../types/nostr.js';
import { KIND } from '../../types/kind-lookup.js';
interface Props {
event: NostrEvent; // The event to react to (kind 11 or 1111)
@ -28,7 +29,7 @@ @@ -28,7 +29,7 @@
// Fetch reactions (kind 7) for this event
const filters = [
{
kinds: [7],
kinds: [KIND.REACTION],
'#e': [event.id]
}
];
@ -59,7 +60,7 @@ @@ -59,7 +60,7 @@
// Normalize reactions for kind 11/1111: only + and - allowed
// Backward compatibility: ⬆ = +, ⬇ = -
let normalizedContent = content;
if (event.kind === 11 || event.kind === 1111) {
if (event.kind === KIND.DISCUSSION_THREAD || event.kind === KIND.COMMENT) {
if (content === '⬆' || content === '↑') {
normalizedContent = '+';
} else if (content === '⬇' || content === '↓') {
@ -90,7 +91,7 @@ @@ -90,7 +91,7 @@
}
// For kind 11/1111, only allow + and -
if ((event.kind === 11 || event.kind === 1111) && content !== '+' && content !== '-') {
if ((event.kind === KIND.DISCUSSION_THREAD || event.kind === KIND.COMMENT) && content !== '+' && content !== '-') {
return;
}
@ -151,7 +152,7 @@ @@ -151,7 +152,7 @@
function getReactionDisplay(content: string): string {
if (content === '+') {
return event.kind === 1 ? '❤' : '↑';
return event.kind === KIND.SHORT_TEXT_NOTE ? '❤' : '↑';
}
if (content === '-') {
return '↓';
@ -167,7 +168,7 @@ @@ -167,7 +168,7 @@
</script>
<div class="reaction-buttons flex gap-2 items-center">
{#if event.kind === 11 || event.kind === 1111}
{#if event.kind === KIND.DISCUSSION_THREAD || event.kind === KIND.COMMENT}
<!-- Thread/Comment reactions: Only + and - -->
<button
onclick={() => toggleReaction('+')}

8
src/lib/modules/threads/ThreadCard.svelte

@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
import { relayManager } from '../../services/nostr/relay-manager.js';
import { onMount } from 'svelte';
import type { NostrEvent } from '../../types/nostr.js';
import { getKindInfo } from '../../types/kind-lookup.js';
import { getKindInfo, KIND } from '../../types/kind-lookup.js';
import { stripMarkdown } from '../../services/text-utils.js';
interface Props {
@ -74,7 +74,7 @@ @@ -74,7 +74,7 @@
// Load reactions (kind 7)
const reactionRelays = relayManager.getThreadReadRelays();
const reactionEvents = await nostrClient.fetchEvents(
[{ kinds: [7], '#e': [thread.id] }],
[{ kinds: [KIND.REACTION], '#e': [thread.id] }],
reactionRelays,
{ useCache: true }
);
@ -92,7 +92,7 @@ @@ -92,7 +92,7 @@
// Load comments (kind 1111)
const commentRelays = relayManager.getCommentReadRelays();
const commentEvents = await nostrClient.fetchEvents(
[{ kinds: [1111], '#E': [thread.id], '#K': ['11'] }],
[{ kinds: [KIND.COMMENT], '#E': [thread.id], '#K': ['11'] }],
commentRelays,
{ useCache: true }
);
@ -101,7 +101,7 @@ @@ -101,7 +101,7 @@
// Load zap receipts (kind 9735)
const zapRelays = relayManager.getZapReceiptReadRelays();
const zapReceipts = await nostrClient.fetchEvents(
[{ kinds: [9735], '#e': [thread.id] }],
[{ kinds: [KIND.ZAP_RECEIPT], '#e': [thread.id] }],
zapRelays,
{ useCache: true }
);

13
src/lib/modules/threads/ThreadList.svelte

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
import ThreadDrawer from '../feed/ThreadDrawer.svelte';
import type { NostrEvent } from '../../types/nostr.js';
import { onMount } from 'svelte';
import { KIND } from '../../types/kind-lookup.js';
// Data maps - all data loaded upfront
let threadsMap = $state<Map<string, NostrEvent>>(new Map()); // threadId -> thread
@ -49,7 +50,7 @@ @@ -49,7 +50,7 @@
// Fetch all threads
const threadEvents = await nostrClient.fetchEvents(
[{ kinds: [11], since, limit: 50 }],
[{ kinds: [KIND.DISCUSSION_THREAD], since, limit: 50 }],
threadRelays,
{
useCache: true,
@ -76,7 +77,7 @@ @@ -76,7 +77,7 @@
if (threadIds.length > 0) {
// Fetch all comments in parallel
const allComments = await nostrClient.fetchEvents(
[{ kinds: [1111], '#E': threadIds, '#K': ['11'] }],
[{ kinds: [KIND.COMMENT], '#E': threadIds, '#K': ['11'] }],
commentRelays,
{ useCache: true }
);
@ -95,7 +96,7 @@ @@ -95,7 +96,7 @@
// Fetch deletion events for current reactions
const deletionEvents = await nostrClient.fetchEvents(
[{ kinds: [5], authors: Array.from(new Set(allReactions.map(r => r.pubkey))) }],
[{ kinds: [KIND.EVENT_DELETION], authors: Array.from(new Set(allReactions.map(r => r.pubkey))) }],
reactionRelays,
{ useCache: true }
);
@ -173,7 +174,7 @@ @@ -173,7 +174,7 @@
};
const reactionsWithLowerE = await nostrClient.fetchEvents(
[{ kinds: [7], '#e': threadIds }],
[{ kinds: [KIND.REACTION], '#e': threadIds }],
reactionRelays,
{
useCache: true,
@ -185,7 +186,7 @@ @@ -185,7 +186,7 @@
let reactionsWithUpperE: NostrEvent[] = [];
try {
reactionsWithUpperE = await nostrClient.fetchEvents(
[{ kinds: [7], '#E': threadIds }],
[{ kinds: [KIND.REACTION], '#E': threadIds }],
reactionRelays,
{
useCache: true,
@ -225,7 +226,7 @@ @@ -225,7 +226,7 @@
// Fetch all zap receipts in parallel
const allZapReceipts = await nostrClient.fetchEvents(
[{ kinds: [9735], '#e': threadIds }],
[{ kinds: [KIND.ZAP_RECEIPT], '#e': threadIds }],
zapRelays,
{ useCache: true }
);

4
src/lib/modules/zaps/ZapButton.svelte

@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
// Fetch profile to get lud16 or lnurl
const config = nostrClient.getConfig();
const profileEvents = await nostrClient.fetchEvents(
[{ kinds: [0], authors: [targetPubkey], limit: 1 }],
[{ kinds: [KIND.METADATA], authors: [targetPubkey], limit: 1 }],
[...config.defaultRelays, ...config.profileRelays],
{ useCache: true }
);
@ -50,7 +50,7 @@ @@ -50,7 +50,7 @@
['p', targetPubkey]
];
if (event.kind !== 0) {
if (event.kind !== KIND.METADATA) {
// Zap to an event, not just a profile
tags.push(['e', event.id]);
tags.push(['k', event.kind.toString()]);

2
src/lib/modules/zaps/ZapReceipt.svelte

@ -35,7 +35,7 @@ @@ -35,7 +35,7 @@
// Fetch zap receipts (kind 9735) for this event
const filters: any[] = [
{
kinds: [9735],
kinds: [KIND.ZAP_RECEIPT],
'#e': [eventId]
}
];

2
src/lib/services/auth/activity-tracker.ts

@ -13,7 +13,7 @@ import type { NostrEvent } from '../../types/nostr.js'; @@ -13,7 +13,7 @@ import type { NostrEvent } from '../../types/nostr.js';
export async function getLastActivity(pubkey: string): Promise<number | undefined> {
// Query for recent events from this pubkey (cache only)
const filters = [
{ authors: [pubkey], kinds: [0, 1, 7, 11, 1111], limit: 1 }
{ authors: [pubkey], kinds: [KIND.METADATA, KIND.SHORT_TEXT_NOTE, KIND.REACTION, KIND.DISCUSSION_THREAD, KIND.COMMENT], limit: 1 }
];
const events = await nostrClient.getByFilters(filters);

3
src/lib/services/cache/profile-cache.ts vendored

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
import { getDB } from './indexeddb-store.js';
import type { NostrEvent } from '../../types/nostr.js';
import { KIND } from '../../types/kind-lookup.js';
export interface CachedProfile {
pubkey: string;
@ -15,7 +16,7 @@ export interface CachedProfile { @@ -15,7 +16,7 @@ export interface CachedProfile {
* Store a profile in cache
*/
export async function cacheProfile(event: NostrEvent): Promise<void> {
if (event.kind !== 0) throw new Error('Not a profile event');
if (event.kind !== KIND.METADATA) throw new Error('Not a profile event');
try {
const db = await getDB();
const cached: CachedProfile = {

5
src/lib/services/nostr/auth-handler.ts

@ -14,6 +14,7 @@ import { fetchRelayLists } from '../user-data.js'; @@ -14,6 +14,7 @@ import { fetchRelayLists } from '../user-data.js';
import { nostrClient } from './nostr-client.js';
import { relayManager } from './relay-manager.js';
import type { NostrEvent } from '../../types/nostr.js';
import { KIND } from '../../types/kind-lookup.js';
// Mute list and blocked relays management
const muteList: Set<string> = new Set();
@ -94,7 +95,7 @@ async function loadUserPreferences(pubkey: string): Promise<void> { @@ -94,7 +95,7 @@ async function loadUserPreferences(pubkey: string): Promise<void> {
// Fetch mute list (kind 10000)
const muteEvents = await nostrClient.fetchEvents(
[{ kinds: [10000], authors: [pubkey], limit: 1 }],
[{ kinds: [KIND.MUTE_LIST], authors: [pubkey], limit: 1 }],
relayManager.getProfileReadRelays(),
{ useCache: true, cacheResults: true }
);
@ -110,7 +111,7 @@ async function loadUserPreferences(pubkey: string): Promise<void> { @@ -110,7 +111,7 @@ async function loadUserPreferences(pubkey: string): Promise<void> {
// Fetch blocked relays (kind 10006)
const blockedRelayEvents = await nostrClient.fetchEvents(
[{ kinds: [10006], authors: [pubkey], limit: 1 }],
[{ kinds: [KIND.BLOCKED_RELAYS], authors: [pubkey], limit: 1 }],
relayManager.getProfileReadRelays(),
{ useCache: true, cacheResults: true }
);

4
src/lib/services/nostr/gif-service.ts

@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
import { nostrClient } from './nostr-client.js';
import type { NostrEvent } from '../../types/nostr.js';
import { KIND_LOOKUP, getKindInfo } from '../../types/kind-lookup.js';
import { KIND_LOOKUP, getKindInfo, KIND } from '../../types/kind-lookup.js';
import { config } from './config.js';
export interface GifMetadata {
@ -182,7 +182,7 @@ export async function fetchGifs(searchQuery?: string, limit: number = 50): Promi @@ -182,7 +182,7 @@ export async function fetchGifs(searchQuery?: string, limit: number = 50): Promi
// This ensures we can still find GIFs even if GIF-specific relays are down
// Only fetch kind 1063 (NIP-94 file metadata) events - kind 1 floods the fetch
const fileMetadataKind = KIND_LOOKUP[1063].number; // NIP-94 File Metadata
const fileMetadataKind = KIND.FILE_METADATA; // NIP-94 File Metadata
// Fetch a larger number of events to build a good cache
// Use a higher limit to ensure we cache enough events for consistent results

11
src/lib/services/nostr/nip30-emoji.ts

@ -13,6 +13,7 @@ @@ -13,6 +13,7 @@
import { nostrClient } from './nostr-client.js';
import { relayManager } from './relay-manager.js';
import type { NostrEvent } from '../../types/nostr.js';
import { KIND } from '../../types/kind-lookup.js';
import { matchAll } from 'nostr-tools/nip30';
export interface EmojiDefinition {
@ -48,7 +49,7 @@ let loadingEmojiPacks = false; @@ -48,7 +49,7 @@ let loadingEmojiPacks = false;
* Parse a kind 10030 emoji set event or kind 30030 emoji pack
*/
export function parseEmojiSet(event: NostrEvent): EmojiSet | null {
if (event.kind !== 10030 && event.kind !== 30030) return null;
if (event.kind !== KIND.EMOJI_SET && event.kind !== KIND.EMOJI_PACK) return null;
const emojis = new Map<string, EmojiDefinition>();
@ -84,7 +85,7 @@ export async function fetchEmojiSet(pubkey: string): Promise<EmojiSet | null> { @@ -84,7 +85,7 @@ export async function fetchEmojiSet(pubkey: string): Promise<EmojiSet | null> {
// Fetch both emoji sets (10030) and emoji packs (30030)
// Get more events to capture all packs (30030 can have multiple with different d tags)
const events = await nostrClient.fetchEvents(
[{ kinds: [10030, 30030], authors: [pubkey], limit: 50 }], // Get more to capture all packs
[{ kinds: [KIND.EMOJI_SET, KIND.EMOJI_PACK], authors: [pubkey], limit: 50 }], // Get more to capture all packs
relays,
{ useCache: true, cacheResults: true, timeout: 5000 }
);
@ -92,8 +93,8 @@ export async function fetchEmojiSet(pubkey: string): Promise<EmojiSet | null> { @@ -92,8 +93,8 @@ export async function fetchEmojiSet(pubkey: string): Promise<EmojiSet | null> {
if (events.length === 0) return null;
// Separate emoji sets (10030) and emoji packs (30030)
const emojiSetEvents = events.filter(e => e.kind === 10030);
const emojiPackEvents = events.filter(e => e.kind === 30030);
const emojiSetEvents = events.filter(e => e.kind === KIND.EMOJI_SET);
const emojiPackEvents = events.filter(e => e.kind === KIND.EMOJI_PACK);
const allEmojis = new Map<string, EmojiDefinition>();
@ -160,7 +161,7 @@ export async function loadAllEmojiPacks(): Promise<void> { @@ -160,7 +161,7 @@ export async function loadAllEmojiPacks(): Promise<void> {
// Fetch all emoji sets (10030) and emoji packs (30030)
// Use a high limit to get all available packs - increase limit to get more
const events = await nostrClient.fetchEvents(
[{ kinds: [10030, 30030], limit: 1000 }], // Increased limit to get more emoji packs/sets
[{ kinds: [KIND.EMOJI_SET, KIND.EMOJI_PACK], limit: 1000 }], // Increased limit to get more emoji packs/sets
relays,
{ useCache: true, cacheResults: true, timeout: 15000 }
);

11
src/lib/services/nostr/nostr-client.ts

@ -10,6 +10,7 @@ import { cacheEvent, cacheEvents, getEvent, getEventsByKind, getEventsByPubkey } @@ -10,6 +10,7 @@ import { cacheEvent, cacheEvents, getEvent, getEventsByKind, getEventsByPubkey }
import { getDB } from '../cache/indexeddb-store.js';
import { filterEvents, shouldHideEvent } from '../event-filter.js';
import { sessionManager } from '../auth/session-manager.js';
import { KIND } from '../../types/kind-lookup.js';
export interface PublishOptions {
relays?: string[];
@ -213,7 +214,7 @@ class NostrClient { @@ -213,7 +214,7 @@ class NostrClient {
}
private shouldFilterZapReceipt(event: NostrEvent): boolean {
if (event.kind !== 9735) return false;
if (event.kind !== KIND.ZAP_RECEIPT) return false;
const amountTag = event.tags.find((t) => t[0] === 'amount');
if (!amountTag || !amountTag[1]) return true;
const amount = parseInt(amountTag[1], 10);
@ -352,6 +353,14 @@ class NostrClient { @@ -352,6 +353,14 @@ class NostrClient {
const dTags = event.tags.filter(t => (t[0] === 'd' || t[0] === 'D') && t[1]).map(t => t[1]);
if (dTags.length === 0 || !filter['#d'].some(d => dTags.includes(d))) continue;
}
if (filter['#q'] && filter['#q'].length > 0) {
const qTags = event.tags.filter(t => (t[0] === 'q' || t[0] === 'Q') && t[1]).map(t => t[1]);
if (qTags.length === 0 || !filter['#q'].some(q => qTags.includes(q))) continue;
}
if (filter['#Q'] && filter['#Q'].length > 0) {
const qTags = event.tags.filter(t => (t[0] === 'q' || t[0] === 'Q') && t[1]).map(t => t[1]);
if (qTags.length === 0 || !filter['#Q'].some(q => qTags.includes(q))) continue;
}
// Use matchFilter for final validation
if (matchFilter(filter, event)) {

13
src/lib/services/user-data.ts

@ -8,6 +8,7 @@ import { relayManager } from './nostr/relay-manager.js'; @@ -8,6 +8,7 @@ import { relayManager } from './nostr/relay-manager.js';
import { cacheProfile, getProfile, getProfiles } from './cache/profile-cache.js';
import { config } from './nostr/config.js';
import type { NostrEvent } from '../types/nostr.js';
import { KIND } from '../types/kind-lookup.js';
// Re-export profile types and functions
export interface ProfileData {
@ -84,7 +85,7 @@ export async function fetchProfile( @@ -84,7 +85,7 @@ export async function fetchProfile(
];
const events = await nostrClient.fetchEvents(
[{ kinds: [0], authors: [pubkey], limit: 1 }],
[{ kinds: [KIND.METADATA], authors: [pubkey], limit: 1 }],
relayList,
{ useCache: true, cacheResults: true }
);
@ -122,7 +123,7 @@ export async function fetchProfiles( @@ -122,7 +123,7 @@ export async function fetchProfiles(
];
const events = await nostrClient.fetchEvents(
[{ kinds: [0], authors: missing, limit: 1 }],
[{ kinds: [KIND.METADATA], authors: missing, limit: 1 }],
relayList,
{ useCache: true, cacheResults: true }
);
@ -139,7 +140,7 @@ export async function fetchProfiles( @@ -139,7 +140,7 @@ export async function fetchProfiles(
* Parse user status from kind 30315 event
*/
export function parseUserStatus(event: NostrEvent): string | null {
if (event.kind !== 30315) return null;
if (event.kind !== KIND.USER_STATUS) return null;
// Check for d tag with value "general"
const dTag = event.tags.find((t) => t[0] === 'd' && t[1] === 'general');
@ -163,7 +164,7 @@ export async function fetchUserStatus( @@ -163,7 +164,7 @@ export async function fetchUserStatus(
const events = await nostrClient.fetchEvents(
[
{
kinds: [30315],
kinds: [KIND.USER_STATUS],
authors: [pubkey],
'#d': ['general'],
limit: 1
@ -236,8 +237,8 @@ export async function fetchRelayLists( @@ -236,8 +237,8 @@ export async function fetchRelayLists(
// Fetch both kind 10002 and 10432
const events = await nostrClient.fetchEvents(
[
{ kinds: [10002], authors: [pubkey], limit: 1 },
{ kinds: [10432], authors: [pubkey], limit: 1 }
{ kinds: [KIND.RELAY_LIST], authors: [pubkey], limit: 1 },
{ kinds: [KIND.LOCAL_RELAYS], authors: [pubkey], limit: 1 }
],
relayList,
{ useCache: true, cacheResults: true }

131
src/lib/types/kind-lookup.ts

@ -11,77 +11,96 @@ export interface KindInfo { @@ -11,77 +11,96 @@ export interface KindInfo {
isSecondaryKind?: boolean; // Whether this is a secondary kind (used to display the main kind)
}
// Kind number constants
export const KIND = {
METADATA: 0,
SHORT_TEXT_NOTE: 1,
CONTACTS: 3,
EVENT_DELETION: 5,
REACTION: 7,
DISCUSSION_THREAD: 11,
COMMENT: 1111,
VOICE_NOTE: 1222,
VOICE_REPLY: 1244,
PICTURE_NOTE: 20,
VIDEO_NOTE: 21,
SHORT_VIDEO_NOTE: 22,
LONG_FORM_NOTE: 30023,
HIGHLIGHTED_ARTICLE: 9802,
FILE_METADATA: 1063,
POLL: 1068,
POLL_RESPONSE: 1018,
USER_STATUS: 30315,
PAYMENT_ADDRESSES: 10133,
LABEL: 1985,
REPORT: 1984,
ZAP_RECEIPT: 9735,
RELAY_LIST: 10002,
BLOCKED_RELAYS: 10006,
FAVORITE_RELAYS: 10012,
LOCAL_RELAYS: 10432,
PIN_LIST: 10001,
BOOKMARKS: 10003,
RSS_FEED: 10895,
INTEREST_LIST: 10015,
EMOJI_SET: 10030,
EMOJI_PACK: 30030,
MUTE_LIST: 10000,
BADGES: 30008,
} as const;
export const KIND_LOOKUP: Record<number, KindInfo> = {
// Core kinds
0: { number: 0, description: 'Metadata', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
1: { number: 1, description: 'Short Text Note', showInFeed: true, isReplaceable: false },
3: { number: 3, description: 'Contacts', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
24: { number: 4, description: 'Public Message', showInFeed: true, isReplaceable: false, isSecondaryKind: false },
5: { number: 5, description: 'Event Deletion', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
7: { number: 7, description: 'Reaction', showInFeed: false, isReplaceable: false, isSecondaryKind: true },
1063: { number: 1063, description: 'File Metadata (GIFs)', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
[KIND.SHORT_TEXT_NOTE]: { number: KIND.SHORT_TEXT_NOTE, description: 'Short Text Note', showInFeed: true, isReplaceable: false },
[KIND.CONTACTS]: { number: KIND.CONTACTS, description: 'Public Message', showInFeed: true, isReplaceable: false, isSecondaryKind: false },
[KIND.EVENT_DELETION]: { number: KIND.EVENT_DELETION, description: 'Event Deletion', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
[KIND.REACTION]: { number: KIND.REACTION, description: 'Reaction', showInFeed: false, isReplaceable: false, isSecondaryKind: true },
// Articles
30023: { number: 30023, description: 'Long-form Note', showInFeed: true, isReplaceable: true, isSecondaryKind: false },
9802: { number: 9802, description: 'Highlighted Article', showInFeed: true, isReplaceable: false, isSecondaryKind: false },
[KIND.LONG_FORM_NOTE]: { number: KIND.LONG_FORM_NOTE, description: 'Long-form Note', showInFeed: true, isReplaceable: true, isSecondaryKind: false },
[KIND.HIGHLIGHTED_ARTICLE]: { number: KIND.HIGHLIGHTED_ARTICLE, description: 'Highlighted Article', showInFeed: true, isReplaceable: false, isSecondaryKind: false },
// Threads and comments
11: { number: 11, description: 'Discussion Thread', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
34550: { number: 34550, description: 'Community', showInFeed: false, isReplaceable: false, isSecondaryKind: true },
1111: { number: 1111, description: 'Comment', showInFeed: true, isReplaceable: false, isSecondaryKind: true },
[KIND.DISCUSSION_THREAD]: { number: KIND.DISCUSSION_THREAD, description: 'Discussion Thread', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
[KIND.COMMENT]: { number: KIND.COMMENT, description: 'Comment', showInFeed: true, isReplaceable: false, isSecondaryKind: true },
// Media
20: { number: 20, description: 'Picture Note', showInFeed: true, isReplaceable: false, isSecondaryKind: false },
21: { number: 21, description: 'Video Note', showInFeed: true, isReplaceable: false, isSecondaryKind: false },
22: { number: 22, description: 'Short Video Note', showInFeed: true, isReplaceable: false, isSecondaryKind: false },
1222: { number: 23, description: 'Voice Note (Yak)', showInFeed: true, isReplaceable: false, isSecondaryKind: false },
1244: { number: 24, description: 'Voice Reply (Yak Back)', showInFeed: false, isReplaceable: false, isSecondaryKind: true },
[KIND.PICTURE_NOTE]: { number: KIND.PICTURE_NOTE, description: 'Picture Note', showInFeed: true, isReplaceable: false, isSecondaryKind: false },
[KIND.VIDEO_NOTE]: { number: KIND.VIDEO_NOTE, description: 'Video Note', showInFeed: true, isReplaceable: false, isSecondaryKind: false },
[KIND.SHORT_VIDEO_NOTE]: { number: KIND.SHORT_VIDEO_NOTE, description: 'Short Video Note', showInFeed: true, isReplaceable: false, isSecondaryKind: false },
[KIND.VOICE_NOTE]: { number: KIND.VOICE_NOTE, description: 'Voice Note (Yak)', showInFeed: true, isReplaceable: false, isSecondaryKind: false },
[KIND.VOICE_REPLY]: { number: KIND.VOICE_REPLY, description: 'Voice Reply (Yak Back)', showInFeed: false, isReplaceable: false, isSecondaryKind: true },
[KIND.FILE_METADATA]: { number: KIND.FILE_METADATA, description: 'File Metadata (GIFs)', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
// Polls
1068: { number: 1068, description: 'Poll', showInFeed: true, isReplaceable: false, isSecondaryKind: false },
1018: { number: 1018, description: 'Poll Response', showInFeed: false, isReplaceable: false, isSecondaryKind: true },
// Labels
1985: { number: 1985, description: 'Label', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
[KIND.POLL]: { number: KIND.POLL, description: 'Poll', showInFeed: true, isReplaceable: false, isSecondaryKind: false },
[KIND.POLL_RESPONSE]: { number: KIND.POLL_RESPONSE, description: 'Poll Response', showInFeed: false, isReplaceable: false, isSecondaryKind: true },
// User status
30315: { number: 30315, description: 'User Status', showInFeed: false, isReplaceable: false, isSecondaryKind: true },
// User events
[KIND.METADATA]: { number: KIND.METADATA, description: 'Metadata', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
[KIND.USER_STATUS]: { number: KIND.USER_STATUS, description: 'User Status', showInFeed: false, isReplaceable: false, isSecondaryKind: true },
[KIND.PAYMENT_ADDRESSES]: { number: KIND.PAYMENT_ADDRESSES, description: 'Payment Addresses', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
[KIND.LABEL]: { number: KIND.LABEL, description: 'Label', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
[KIND.REPORT]: { number: KIND.REPORT, description: 'Report', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
// Zaps
9735: { number: 9735, description: 'Zap Receipt', showInFeed: false, isReplaceable: false, isSecondaryKind: true },
[KIND.ZAP_RECEIPT]: { number: KIND.ZAP_RECEIPT, description: 'Zap Receipt', showInFeed: false, isReplaceable: false, isSecondaryKind: true },
// Relay lists
10002: { number: 10002, description: 'Relay List Metadata', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
// Blocked relays
10006: { number: 10006, description: 'Blocked Relays', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
// Favorite relays
10012: { number: 10012, description: 'Favorite Relays', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
// Interest lists
10015: { number: 10015, description: 'Interest List', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
// Local relays
10432: { number: 10432, description: 'Local Relays', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
// Mute lists
10000: { number: 10000, description: 'Mute List', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
// Pin lists
10001: { number: 10001, description: 'Pin List', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
10009: { number: 10009, description: 'Bookmarks', showInFeed: false, isReplaceable: false, isSecondaryKind: true },
// Payment addresses
10133: { number: 10133, description: 'Payment Addresses', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
// Custom emojis (NIP-30)
10030: { number: 10030, description: 'Emoji Set', showInFeed: false, isReplaceable: true, isSecondaryKind: false },
30030: { number: 30030, description: 'Emoji Pack', showInFeed: false, isReplaceable: true, isSecondaryKind: false },
// RSS feeds
10895: { number: 10895, description: 'RSS Feed', showInFeed: false, isReplaceable: false }
[KIND.RELAY_LIST]: { number: KIND.RELAY_LIST, description: 'Relay List Metadata', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
[KIND.BLOCKED_RELAYS]: { number: KIND.BLOCKED_RELAYS, description: 'Blocked Relays', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
[KIND.FAVORITE_RELAYS]: { number: KIND.FAVORITE_RELAYS, description: 'Favorite Relays', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
[KIND.LOCAL_RELAYS]: { number: KIND.LOCAL_RELAYS, description: 'Local Relays', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
// Personal lists
[KIND.PIN_LIST]: { number: KIND.PIN_LIST, description: 'Pin List', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
[KIND.BOOKMARKS]: { number: KIND.BOOKMARKS, description: 'Bookmarks', showInFeed: false, isReplaceable: false, isSecondaryKind: true },
[KIND.RSS_FEED]: { number: KIND.RSS_FEED, description: 'RSS Feed', showInFeed: false, isReplaceable: false },
[KIND.INTEREST_LIST]: { number: KIND.INTEREST_LIST, description: 'Interest List', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
[KIND.EMOJI_SET]: { number: KIND.EMOJI_SET, description: 'Emoji Set', showInFeed: false, isReplaceable: true, isSecondaryKind: false },
[KIND.EMOJI_PACK]: { number: KIND.EMOJI_PACK, description: 'Emoji Pack', showInFeed: false, isReplaceable: true, isSecondaryKind: false },
[KIND.MUTE_LIST]: { number: KIND.MUTE_LIST, description: 'Mute List', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
[KIND.BADGES]: { number: KIND.BADGES, description: 'Badges', showInFeed: false, isReplaceable: false, isSecondaryKind: false },
};
/**

Loading…
Cancel
Save