Browse Source

bug-fixes

master
Silberengel 4 weeks ago
parent
commit
a7d24a8204
  1. 47
      src/lib/components/EventMenu.svelte
  2. 25
      src/lib/modules/feed/FeedPage.svelte
  3. 26
      src/lib/services/cache/event-cache.ts
  4. 64
      src/lib/services/user-actions.ts
  5. 6
      src/lib/types/kind-lookup.ts
  6. 711
      src/lib/types/kind-metadata.ts

47
src/lib/components/EventMenu.svelte

@ -14,7 +14,9 @@ @@ -14,7 +14,9 @@
isHighlighted,
togglePin,
toggleBookmark,
toggleHighlight
toggleHighlight,
toggleMute,
isMuted
} from '../services/user-actions.js';
import { eventMenuStore } from '../services/event-menu-store.js';
import { sessionManager } from '../services/auth/session-manager.js';
@ -68,10 +70,12 @@ @@ -68,10 +70,12 @@
let currentUserPubkey = $derived(sessionManager.getCurrentPubkey());
let isOwnEvent = $derived(isLoggedIn && currentUserPubkey === event.pubkey);
// Track pin/bookmark/highlight state
// Track pin/bookmark/highlight/mute state
let pinnedState = $state(false);
let bookmarkedState = $state(false);
let highlightedState = $state(false);
let mutedState = $state(false);
let muting = $state(false);
let stateUpdateTrigger = $state(0); // Trigger to force state updates
// Update state when event changes or when trigger changes
@ -87,6 +91,12 @@ @@ -87,6 +91,12 @@
isBookmarked(event.id).then(bookmarked => {
bookmarkedState = bookmarked;
});
// Update mute state
if (isLoggedIn && !isOwnEvent) {
isMuted(event.pubkey).then(muted => {
mutedState = muted;
});
}
});
function toggleMenu() {
@ -399,6 +409,22 @@ @@ -399,6 +409,22 @@
ttsModalOpen = true;
closeMenu();
}
async function handleMute() {
if (muting || isOwnEvent) return;
muting = true;
try {
await toggleMute(event.pubkey);
// Update state
const newMuted = await isMuted(event.pubkey);
mutedState = newMuted;
} catch (error) {
console.error('Failed to toggle mute:', error);
} finally {
muting = false;
closeMenu();
}
}
</script>
<div class="event-menu-container">
@ -521,9 +547,24 @@ @@ -521,9 +547,24 @@
</button>
{/if}
<!-- Report action (logged in and not own event) -->
<!-- Mute action (logged in and not own event) -->
{#if isLoggedIn && !isOwnEvent}
<div class="menu-divider"></div>
<button
class="menu-item"
onclick={handleMute}
disabled={muting}
>
<Icon name={mutedState ? "message-square" : "x"} size={16} />
<span>{mutedState ? 'Unmute this user' : 'Mute this user'}</span>
{#if mutedState}
<span class="action-indicator"></span>
{/if}
</button>
{/if}
<!-- Report action (logged in and not own event) -->
{#if isLoggedIn && !isOwnEvent}
<button class="menu-item menu-item-danger" onclick={openReportModal}>
<Icon name="x" size={16} />
<span>Report this event</span>

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

@ -13,6 +13,7 @@ @@ -13,6 +13,7 @@
import Pagination from '../../components/ui/Pagination.svelte';
import { getPaginatedItems, getCurrentPage, ITEMS_PER_PAGE } from '../../utils/pagination.js';
import { isReply } from '../../utils/event-utils.js';
import { filterEvents } from '../../services/event-filter.js';
interface Props {
singleRelay?: string;
@ -76,11 +77,13 @@ @@ -76,11 +77,13 @@
}
}
// Filter events based on showOnlyOPs
// Filter events based on showOnlyOPs and mute list
let events = $derived(
filterEvents(
showOnlyOPs
? allEvents.filter(event => !isReply(event))
: allEvents
)
);
// Pagination
@ -177,9 +180,12 @@ @@ -177,9 +180,12 @@
if (!isMounted) return;
const filtered = fetched.filter(e =>
// Filter by kind and mute list
const filtered = filterEvents(
fetched.filter(e =>
e.kind !== KIND.DISCUSSION_THREAD &&
getKindInfo(e.kind).showInFeed === true
)
);
if (filtered.length === 0) {
@ -218,9 +224,12 @@ @@ -218,9 +224,12 @@
if (!singleRelay) {
const feedKinds = getFeedKinds().filter(k => k !== KIND.DISCUSSION_THREAD);
const cached = await getRecentFeedEvents(feedKinds, 60 * 60 * 1000, config.feedLimit); // 1 hour cache
const filtered = cached.filter(e =>
// Filter by kind and mute list
const filtered = filterEvents(
cached.filter(e =>
e.kind !== KIND.DISCUSSION_THREAD &&
getKindInfo(e.kind).showInFeed === true
)
);
if (filtered.length > 0 && isMounted) {
@ -263,9 +272,12 @@ @@ -263,9 +272,12 @@
onUpdate: (newEvents) => {
if (!isMounted) return;
const filtered = newEvents.filter(e =>
// Filter by kind and mute list
const filtered = filterEvents(
newEvents.filter(e =>
e.kind !== KIND.DISCUSSION_THREAD &&
getKindInfo(e.kind).showInFeed === true
)
);
if (filtered.length === 0) return;
@ -291,9 +303,12 @@ @@ -291,9 +303,12 @@
if (!isMounted) return;
// Final merge of any remaining events (for single relay mode or fallback)
const filtered = fetched.filter(e =>
// Filter by kind and mute list
const filtered = filterEvents(
fetched.filter(e =>
e.kind !== KIND.DISCUSSION_THREAD &&
getKindInfo(e.kind).showInFeed === true
)
);
if (filtered.length > 0) {

26
src/lib/services/cache/event-cache.ts vendored

@ -243,6 +243,32 @@ export async function deleteEvents(ids: string[]): Promise<void> { @@ -243,6 +243,32 @@ export async function deleteEvents(ids: string[]): Promise<void> {
await tx.done;
}
/**
* Delete all events by pubkey from cache
*/
export async function deleteEventsByPubkey(pubkey: string): Promise<number> {
try {
const db = await getDB();
const tx = db.transaction('events', 'readwrite');
const index = tx.store.index('pubkey');
const events = await index.getAll(pubkey);
await tx.done;
if (events.length === 0) return 0;
// Delete in batch
const deleteTx = db.transaction('events', 'readwrite');
await Promise.all(events.map(e => deleteTx.store.delete(e.id)));
await deleteTx.done;
return events.length;
} catch (error) {
console.error('Error deleting events by pubkey:', error);
return 0;
}
}
/**
* Get recent events from cache by kind(s) (within cache TTL)
* Returns events that were cached recently and match the specified kinds

64
src/lib/services/user-actions.ts

@ -537,6 +537,7 @@ function invalidateMuteCache() { @@ -537,6 +537,7 @@ function invalidateMuteCache() {
/**
* Toggle mute status of a user
* Publishes kind 10000 mute list event (mutes are stored in cache and on relays only)
* When muting, deletes all events from that user from cache and archive
*/
export async function toggleMute(pubkey: string): Promise<boolean> {
try {
@ -554,11 +555,30 @@ export async function toggleMute(pubkey: string): Promise<boolean> { @@ -554,11 +555,30 @@ export async function toggleMute(pubkey: string): Promise<boolean> {
currentMutes.delete(pubkey);
} else {
currentMutes.add(pubkey);
// When muting, delete all events from this user from cache and archive
const { deleteEventsByPubkey } = await import('./cache/event-cache.js');
const { deleteArchivedEventsByPubkey } = await import('./cache/event-archive.js');
try {
const deletedFromCache = await deleteEventsByPubkey(pubkey);
const deletedFromArchive = await deleteArchivedEventsByPubkey(pubkey);
console.log(`[Mute] Deleted ${deletedFromCache} events from cache and ${deletedFromArchive} events from archive for muted user ${pubkey.substring(0, 8)}...`);
} catch (error) {
console.error('Error deleting events for muted user:', error);
// Continue with mute even if deletion fails
}
}
// Publish updated mute list event
await publishMuteList(Array.from(currentMutes));
// Update auth-handler mute list immediately (for real-time filtering)
const { getMuteList } = await import('./nostr/auth-handler.js');
const muteList = getMuteList();
muteList.clear();
currentMutes.forEach(pk => muteList.add(pk));
// Invalidate cache so next read gets fresh data
invalidateMuteCache();
@ -590,9 +610,10 @@ async function publishMuteList(pubkeys: string[]): Promise<void> { @@ -590,9 +610,10 @@ async function publishMuteList(pubkeys: string[]): Promise<void> {
{ useCache: true, cacheResults: true }
);
// Collect existing p tags
// Collect existing tags - preserve ALL non-p tags (t, e, word, etc.) and manage p tags
const existingPubkeys = new Set<string>();
const existingPTags: string[][] = []; // Store full p tags to preserve relay hints and petnames
const otherTags: string[][] = []; // Store all non-p tags (t, e, word, etc.)
if (existingLists.length > 0) {
const existingList = existingLists[0];
@ -600,11 +621,14 @@ async function publishMuteList(pubkeys: string[]): Promise<void> { @@ -600,11 +621,14 @@ async function publishMuteList(pubkeys: string[]): Promise<void> {
if (tag[0] === 'p' && tag[1]) {
existingPubkeys.add(tag[1]);
existingPTags.push(tag);
} else if (tag[0] && tag[0] !== 'p') {
// Preserve all non-p tags (t, e, word, etc.)
otherTags.push(tag);
}
}
}
// Check if we have any changes
// Check if we have any changes to p tags
const newPubkeys = deduplicatedPubkeys.filter(p => !existingPubkeys.has(p));
const removedPubkeys = [...existingPubkeys].filter(p => !deduplicatedPubkeys.includes(p));
@ -612,11 +636,16 @@ async function publishMuteList(pubkeys: string[]): Promise<void> { @@ -612,11 +636,16 @@ async function publishMuteList(pubkeys: string[]): Promise<void> {
return; // No changes, cancel operation
}
// Build final tags: preserve existing p tags for pubkeys we're keeping, add new ones
// Build final tags: preserve existing p tags for pubkeys we're keeping, add new ones, preserve all other tags
const tags: string[][] = [];
const seenPubkeys = new Set<string>();
// First, add existing p tags for pubkeys we're keeping
// First, preserve all non-p tags (t, e, word, etc.)
for (const tag of otherTags) {
tags.push(tag);
}
// Then, add existing p tags for pubkeys we're keeping
for (const tag of existingPTags) {
if (tag[1] && deduplicatedPubkeys.includes(tag[1])) {
tags.push(tag);
@ -624,7 +653,7 @@ async function publishMuteList(pubkeys: string[]): Promise<void> { @@ -624,7 +653,7 @@ async function publishMuteList(pubkeys: string[]): Promise<void> {
}
}
// Then, add new p tags for pubkeys we're adding (without relay hints or petnames)
// Finally, add new p tags for pubkeys we're adding (without relay hints or petnames)
for (const pubkey of deduplicatedPubkeys) {
if (!seenPubkeys.has(pubkey)) {
tags.push(['p', pubkey]);
@ -643,6 +672,12 @@ async function publishMuteList(pubkeys: string[]): Promise<void> { @@ -643,6 +672,12 @@ async function publishMuteList(pubkeys: string[]): Promise<void> {
// Publish to write relays
const writeRelays = relayManager.getPublishRelays(relays, true);
await signAndPublish(listEvent, writeRelays);
// Update auth-handler mute list immediately (for real-time filtering)
const { getMuteList } = await import('./nostr/auth-handler.js');
const muteList = getMuteList();
muteList.clear();
deduplicatedPubkeys.forEach(pk => muteList.add(pk));
} catch (error) {
console.error('Failed to publish mute list:', error);
}
@ -765,9 +800,10 @@ async function publishFollowList(pubkeys: string[]): Promise<void> { @@ -765,9 +800,10 @@ async function publishFollowList(pubkeys: string[]): Promise<void> {
{ useCache: true, cacheResults: true }
);
// Collect existing p tags
// Collect existing tags - preserve ALL non-p tags and manage p tags
const existingPubkeys = new Set<string>();
const existingPTags: string[][] = []; // Store full p tags to preserve relay hints and petnames
const otherTags: string[][] = []; // Store all non-p tags (for future extensibility)
if (existingLists.length > 0) {
const existingList = existingLists[0];
@ -775,11 +811,14 @@ async function publishFollowList(pubkeys: string[]): Promise<void> { @@ -775,11 +811,14 @@ async function publishFollowList(pubkeys: string[]): Promise<void> {
if (tag[0] === 'p' && tag[1]) {
existingPubkeys.add(tag[1]);
existingPTags.push(tag);
} else if (tag[0] && tag[0] !== 'p') {
// Preserve all non-p tags
otherTags.push(tag);
}
}
}
// Check if we have any changes
// Check if we have any changes to p tags
const newPubkeys = deduplicatedPubkeys.filter(p => !existingPubkeys.has(p));
const removedPubkeys = [...existingPubkeys].filter(p => !deduplicatedPubkeys.includes(p));
@ -787,11 +826,16 @@ async function publishFollowList(pubkeys: string[]): Promise<void> { @@ -787,11 +826,16 @@ async function publishFollowList(pubkeys: string[]): Promise<void> {
return; // No changes, cancel operation
}
// Build final tags: preserve existing p tags for pubkeys we're keeping, add new ones
// Build final tags: preserve existing p tags for pubkeys we're keeping, add new ones, preserve all other tags
const tags: string[][] = [];
const seenPubkeys = new Set<string>();
// First, add existing p tags for pubkeys we're keeping
// First, preserve all non-p tags
for (const tag of otherTags) {
tags.push(tag);
}
// Then, add existing p tags for pubkeys we're keeping
for (const tag of existingPTags) {
if (tag[1] && deduplicatedPubkeys.includes(tag[1])) {
tags.push(tag);
@ -799,7 +843,7 @@ async function publishFollowList(pubkeys: string[]): Promise<void> { @@ -799,7 +843,7 @@ async function publishFollowList(pubkeys: string[]): Promise<void> {
}
}
// Then, add new p tags for pubkeys we're adding (without relay hints or petnames)
// Finally, add new p tags for pubkeys we're adding (without relay hints or petnames)
for (const pubkey of deduplicatedPubkeys) {
if (!seenPubkeys.has(pubkey)) {
tags.push(['p', pubkey]);

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

@ -195,3 +195,9 @@ export function getFeedKinds(): number[] { @@ -195,3 +195,9 @@ export function getFeedKinds(): number[] {
.map(kind => kind.number);
}
/**
* Get all kinds from KIND_LOOKUP
*/
export function getAllKinds(): number[] {
return Object.keys(KIND_LOOKUP).map(k => parseInt(k, 10));
}

711
src/lib/types/kind-metadata.ts

@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
* Based on NIPs from nips-silberengel directory
*/
import { KIND, KIND_LOOKUP, type KindInfo } from './kind-lookup.js';
import { KIND, KIND_LOOKUP, getAllKinds, type KindInfo } from './kind-lookup.js';
export interface KindMetadata extends KindInfo {
helpText: {
@ -15,24 +15,6 @@ export interface KindMetadata extends KindInfo { @@ -15,24 +15,6 @@ export interface KindMetadata extends KindInfo {
requiresContent?: boolean; // Whether content field is required (default: true)
}
// Kinds that can be written via the form
const WRITABLE_KINDS = [
KIND.SHORT_TEXT_NOTE,
KIND.DISCUSSION_THREAD,
KIND.PICTURE_NOTE,
KIND.VIDEO_NOTE,
KIND.SHORT_VIDEO_NOTE,
KIND.PUBLIC_MESSAGE,
KIND.POLL,
KIND.VOICE_NOTE,
KIND.HIGHLIGHTED_ARTICLE,
KIND.RSS_FEED,
KIND.LONG_FORM_NOTE,
KIND.PUBLICATION_INDEX,
KIND.PUBLICATION_CONTENT,
KIND.WIKI_MARKDOWN,
KIND.WIKI_ASCIIDOC,
] as const;
export const KIND_METADATA: Record<number, KindMetadata> = {
[KIND.SHORT_TEXT_NOTE]: {
@ -375,6 +357,694 @@ export const KIND_METADATA: Record<number, KindMetadata> = { @@ -375,6 +357,694 @@ export const KIND_METADATA: Record<number, KindMetadata> = {
sig: '...'
})
},
[KIND.METADATA]: {
...KIND_LOOKUP[KIND.METADATA],
writable: true,
requiresContent: false,
helpText: {
description: 'Profile Metadata (NIP-01). Replaceable event that stores user profile information. The content is a JSON string containing profile data.',
suggestedTags: []
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.METADATA,
pubkey,
created_at: timestamp,
content: JSON.stringify({
name: 'Alice',
about: 'Nostr user',
picture: 'https://example.com/avatar.jpg',
nip05: 'alice@example.com'
}),
tags: [],
id: '...',
sig: '...'
})
},
[KIND.CONTACTS]: {
...KIND_LOOKUP[KIND.CONTACTS],
writable: true,
requiresContent: false,
helpText: {
description: 'Contact List (NIP-02). Replaceable event that stores a list of contacts (people the user follows). Content should be empty. p tags contain pubkeys of contacts.',
suggestedTags: ['p (contact pubkeys)', 'relay (optional relay hints)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.CONTACTS,
pubkey,
created_at: timestamp,
content: '',
tags: [
['p', '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d', relay],
['p', '67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446', relay]
],
id: '...',
sig: '...'
})
},
[KIND.EVENT_DELETION]: {
...KIND_LOOKUP[KIND.EVENT_DELETION],
writable: true,
requiresContent: false,
helpText: {
description: 'Event Deletion (NIP-09). Used to request deletion of events. Content should be empty or contain a deletion reason. e tags reference the events to delete.',
suggestedTags: ['e (event IDs to delete)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.EVENT_DELETION,
pubkey,
created_at: timestamp,
content: '',
tags: [
['e', eventId]
],
id: '...',
sig: '...'
})
},
[KIND.DELETION_REQUEST]: {
...KIND_LOOKUP[KIND.DELETION_REQUEST],
writable: true,
helpText: {
description: 'Deletion Request. Used to request deletion of events from relays. Content may contain a reason.',
suggestedTags: ['e (event IDs to delete)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.DELETION_REQUEST,
pubkey,
created_at: timestamp,
content: 'Please delete this event',
tags: [
['e', eventId]
],
id: '...',
sig: '...'
})
},
[KIND.REACTION]: {
...KIND_LOOKUP[KIND.REACTION],
writable: true,
helpText: {
description: 'Reaction (NIP-25). Used to react to events. Content is typically a single emoji or +/-, but can be any string. e tag references the event being reacted to.',
suggestedTags: ['e (event ID being reacted to)', 'p (author of the event being reacted to)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.REACTION,
pubkey,
created_at: timestamp,
content: '+',
tags: [
['e', eventId, relay],
['p', pubkey]
],
id: '...',
sig: '...'
})
},
[KIND.COMMENT]: {
...KIND_LOOKUP[KIND.COMMENT],
writable: true,
helpText: {
description: 'Comment (NIP-22). Comments on threads or other events. e tag references the event being commented on. root e tag references the thread root.',
suggestedTags: ['e (event being commented on)', 'root (thread root event)', 'p (author of parent event)', 't (hashtags)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.COMMENT,
pubkey,
created_at: timestamp,
content: 'This is a comment on a thread.',
tags: [
['e', eventId, relay, 'root'],
['e', eventId, relay, 'reply'],
['p', pubkey]
],
id: '...',
sig: '...'
})
},
[KIND.VOICE_REPLY]: {
...KIND_LOOKUP[KIND.VOICE_REPLY],
writable: true,
helpText: {
description: 'Voice Reply (NIP-A0). Reply messages for short voice messages. Content MUST be a URL pointing directly to an audio file. e tag references the event being replied to.',
suggestedTags: ['e (event being replied to)', 'p (author of parent event)', 'imeta (with url, waveform, duration)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.VOICE_REPLY,
pubkey,
created_at: timestamp,
content: 'https://example.com/audio/reply.m4a',
tags: [
['e', eventId, relay],
['p', pubkey],
['imeta', 'url https://example.com/audio/reply.m4a', 'waveform 0 5 20 6 80 90 30', 'duration 5']
],
id: '...',
sig: '...'
})
},
[KIND.FILE_METADATA]: {
...KIND_LOOKUP[KIND.FILE_METADATA],
writable: true,
helpText: {
description: 'File Metadata (NIP-94). Used to share file metadata, particularly for GIFs. Content should be empty. imeta tag contains file information.',
suggestedTags: ['imeta (url, m, dim, blurhash, x, alt, fallback)', 't (hashtags)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.FILE_METADATA,
pubkey,
created_at: timestamp,
content: '',
tags: [
['imeta', 'url https://example.com/file.gif', 'm image/gif', 'dim 500x500', 'x abc123'],
['t', 'gif']
],
id: '...',
sig: '...'
})
},
[KIND.POLL_RESPONSE]: {
...KIND_LOOKUP[KIND.POLL_RESPONSE],
writable: true,
requiresContent: false,
helpText: {
description: 'Poll Response (NIP-88). Used to respond to polls. Content should be empty. e tag references the poll event, option tag specifies the selected option(s).',
suggestedTags: ['e (poll event ID)', 'option (selected option IDs)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.POLL_RESPONSE,
pubkey,
created_at: timestamp,
content: '',
tags: [
['e', eventId, relay],
['option', 'opt1']
],
id: '...',
sig: '...'
})
},
[KIND.USER_STATUS]: {
...KIND_LOOKUP[KIND.USER_STATUS],
writable: true,
helpText: {
description: 'User Status (NIP-38). Short status updates from users. Content contains the status message.',
suggestedTags: ['expiration (optional unix timestamp)', 't (hashtags)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.USER_STATUS,
pubkey,
created_at: timestamp,
content: 'Working on a new project!',
tags: [
['expiration', (timestamp + 86400).toString()],
['t', 'status']
],
id: '...',
sig: '...'
})
},
[KIND.PAYMENT_ADDRESSES]: {
...KIND_LOOKUP[KIND.PAYMENT_ADDRESSES],
writable: true,
requiresContent: false,
helpText: {
description: 'Payment Addresses (NIP-47). Stores payment addresses (e.g., Lightning addresses). Content should be empty. p tags contain payment addresses.',
suggestedTags: ['p (payment addresses)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.PAYMENT_ADDRESSES,
pubkey,
created_at: timestamp,
content: '',
tags: [
['p', 'alice@example.com']
],
id: '...',
sig: '...'
})
},
[KIND.LABEL]: {
...KIND_LOOKUP[KIND.LABEL],
writable: true,
helpText: {
description: 'Label (NIP-32). Used to label events or users. Content contains the label text.',
suggestedTags: ['e (event IDs to label)', 'p (pubkeys to label)', 'L (label namespace)', 'l (label value)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.LABEL,
pubkey,
created_at: timestamp,
content: 'Important',
tags: [
['e', eventId],
['L', 'content'],
['l', 'important']
],
id: '...',
sig: '...'
})
},
[KIND.REPORT]: {
...KIND_LOOKUP[KIND.REPORT],
writable: true,
helpText: {
description: 'Report (NIP-56). Used to report content or users. Content contains the report reason.',
suggestedTags: ['e (event IDs being reported)', 'p (pubkeys being reported)', 'report-type (spam|impersonation|nudity|profanity|illegal|scam)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.REPORT,
pubkey,
created_at: timestamp,
content: 'This content violates community guidelines',
tags: [
['e', eventId],
['report-type', 'spam']
],
id: '...',
sig: '...'
})
},
[KIND.RELAY_LIST]: {
...KIND_LOOKUP[KIND.RELAY_LIST],
writable: true,
requiresContent: false,
helpText: {
description: 'Relay List Metadata (NIP-65). Stores relay information and preferences. Content should be empty. relay tags contain relay URLs.',
suggestedTags: ['relay (relay URLs)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.RELAY_LIST,
pubkey,
created_at: timestamp,
content: '',
tags: [
['relay', 'wss://relay.example.com', 'read', 'write']
],
id: '...',
sig: '...'
})
},
[KIND.BLOCKED_RELAYS]: {
...KIND_LOOKUP[KIND.BLOCKED_RELAYS],
writable: true,
requiresContent: false,
helpText: {
description: 'Blocked Relays. List of relays to block. Content should be empty. relay tags contain blocked relay URLs.',
suggestedTags: ['relay (blocked relay URLs)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.BLOCKED_RELAYS,
pubkey,
created_at: timestamp,
content: '',
tags: [
['relay', 'wss://bad-relay.com']
],
id: '...',
sig: '...'
})
},
[KIND.FAVORITE_RELAYS]: {
...KIND_LOOKUP[KIND.FAVORITE_RELAYS],
writable: true,
requiresContent: false,
helpText: {
description: 'Favorite Relays. List of favorite relays. Content should be empty. relay tags contain favorite relay URLs.',
suggestedTags: ['relay (favorite relay URLs)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.FAVORITE_RELAYS,
pubkey,
created_at: timestamp,
content: '',
tags: [
['relay', 'wss://relay.example.com']
],
id: '...',
sig: '...'
})
},
[KIND.LOCAL_RELAYS]: {
...KIND_LOOKUP[KIND.LOCAL_RELAYS],
writable: true,
requiresContent: false,
helpText: {
description: 'Local Relays. List of local relays. Content should be empty. relay tags contain local relay URLs.',
suggestedTags: ['relay (local relay URLs)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.LOCAL_RELAYS,
pubkey,
created_at: timestamp,
content: '',
tags: [
['relay', 'ws://localhost:8080']
],
id: '...',
sig: '...'
})
},
[KIND.PIN_LIST]: {
...KIND_LOOKUP[KIND.PIN_LIST],
writable: true,
requiresContent: false,
helpText: {
description: 'Pin List. List of pinned events. Content should be empty. e tags contain event IDs to pin.',
suggestedTags: ['e (pinned event IDs)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.PIN_LIST,
pubkey,
created_at: timestamp,
content: '',
tags: [
['e', eventId]
],
id: '...',
sig: '...'
})
},
[KIND.BOOKMARKS]: {
...KIND_LOOKUP[KIND.BOOKMARKS],
writable: true,
requiresContent: false,
helpText: {
description: 'Bookmarks (NIP-51). List of bookmarked events. Content should be empty. e tags contain bookmarked event IDs.',
suggestedTags: ['e (bookmarked event IDs)', 'a (addressable events)', 't (hashtags for categorization)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.BOOKMARKS,
pubkey,
created_at: timestamp,
content: '',
tags: [
['e', eventId, relay],
['t', 'favorites']
],
id: '...',
sig: '...'
})
},
[KIND.INTEREST_LIST]: {
...KIND_LOOKUP[KIND.INTEREST_LIST],
writable: true,
requiresContent: false,
helpText: {
description: 'Interest List. List of topics or interests. Content should be empty. t tags contain interest topics.',
suggestedTags: ['t (interest topics/hashtags)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.INTEREST_LIST,
pubkey,
created_at: timestamp,
content: '',
tags: [
['t', 'nostr'],
['t', 'bitcoin'],
['t', 'technology']
],
id: '...',
sig: '...'
})
},
[KIND.EMOJI_SET]: {
...KIND_LOOKUP[KIND.EMOJI_SET],
writable: true,
requiresContent: false,
helpText: {
description: 'Emoji Set (NIP-30). Custom emoji set. Content should be empty. emoji tags define custom emojis.',
suggestedTags: ['emoji (shortcode, URL)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.EMOJI_SET,
pubkey,
created_at: timestamp,
content: '',
tags: [
['emoji', 'nostr', 'https://example.com/emoji/nostr.png']
],
id: '...',
sig: '...'
})
},
[KIND.EMOJI_PACK]: {
...KIND_LOOKUP[KIND.EMOJI_PACK],
writable: true,
requiresContent: false,
helpText: {
description: 'Emoji Pack (NIP-30). Collection of emoji sets. Content should be empty. a tags reference emoji set events.',
suggestedTags: ['a (emoji set event references)', 'd (pack identifier)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.EMOJI_PACK,
pubkey,
created_at: timestamp,
content: '',
tags: [
['d', 'my-emoji-pack'],
['a', `10030:${pubkey}:emoji-set-1`, relay]
],
id: '...',
sig: '...'
})
},
[KIND.MUTE_LIST]: {
...KIND_LOOKUP[KIND.MUTE_LIST],
writable: true,
requiresContent: false,
helpText: {
description: 'Mute List (NIP-51). List of muted users or content. Content should be empty. p tags contain muted pubkeys, t tags contain muted hashtags.',
suggestedTags: ['p (muted pubkeys)', 't (muted hashtags)', 'e (muted event IDs)', 'word (muted words)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.MUTE_LIST,
pubkey,
created_at: timestamp,
content: '',
tags: [
['p', '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d'],
['t', 'spam']
],
id: '...',
sig: '...'
})
},
[KIND.BADGES]: {
...KIND_LOOKUP[KIND.BADGES],
writable: true,
requiresContent: false,
helpText: {
description: 'Badges (NIP-58). Badge definitions. Content should be empty. d tag contains badge identifier, name tag contains badge name.',
suggestedTags: ['d (badge identifier)', 'name (badge name)', 'description', 'image (badge image URL)', 'thumb (thumbnail URL)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.BADGES,
pubkey,
created_at: timestamp,
content: '',
tags: [
['d', 'early-adopter'],
['name', 'Early Adopter'],
['description', 'For users who joined early'],
['image', 'https://example.com/badge.png']
],
id: '...',
sig: '...'
})
},
[KIND.FOLLOW_SET]: {
...KIND_LOOKUP[KIND.FOLLOW_SET],
writable: true,
requiresContent: false,
helpText: {
description: 'Follow Set (NIP-51). List of followed users. Content should be empty. p tags contain followed pubkeys.',
suggestedTags: ['p (followed pubkeys)', 'relay (optional relay hints)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.FOLLOW_SET,
pubkey,
created_at: timestamp,
content: '',
tags: [
['p', '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d', relay],
['p', '67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446', relay]
],
id: '...',
sig: '...'
})
},
[KIND.HTTP_AUTH]: {
...KIND_LOOKUP[KIND.HTTP_AUTH],
writable: true,
helpText: {
description: 'HTTP Auth (NIP-98). Used for HTTP authentication with Nostr. Content contains the HTTP method and URL.',
suggestedTags: ['u (URL)', 'method (HTTP method)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.HTTP_AUTH,
pubkey,
created_at: timestamp,
content: 'GET https://example.com/api',
tags: [
['u', 'https://example.com/api'],
['method', 'GET']
],
id: '...',
sig: '...'
})
},
[KIND.REPO_ANNOUNCEMENT]: {
...KIND_LOOKUP[KIND.REPO_ANNOUNCEMENT],
writable: true,
helpText: {
description: 'Repository Announcement (NIP-34/GRASP). Announces a Git repository. Content contains repository description.',
suggestedTags: ['d (repository identifier)', 'name (repository name)', 'about', 'web (repository URL)', 'git (git URL)', 'relays (relay URLs)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.REPO_ANNOUNCEMENT,
pubkey,
created_at: timestamp,
content: 'A Nostr-based Git repository',
tags: [
['d', 'my-repo'],
['name', 'My Repository'],
['about', 'Repository description'],
['web', 'https://example.com/repo'],
['git', 'https://git.example.com/repo.git']
],
id: '...',
sig: '...'
})
},
[KIND.ISSUE]: {
...KIND_LOOKUP[KIND.ISSUE],
writable: true,
helpText: {
description: 'Issue (NIP-34/GRASP). Git repository issue. Content contains the issue description.',
suggestedTags: ['a (repository reference)', 'title', 'status (open|closed)', 'assignee (pubkey)', 'label']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.ISSUE,
pubkey,
created_at: timestamp,
content: 'Issue description and details',
tags: [
['a', `30617:${pubkey}:my-repo`, relay],
['title', 'Bug Report'],
['status', 'open']
],
id: '...',
sig: '...'
})
},
[KIND.STATUS_OPEN]: {
...KIND_LOOKUP[KIND.STATUS_OPEN],
writable: true,
helpText: {
description: 'Status: Open (NIP-34/GRASP). Marks an issue or pull request as open.',
suggestedTags: ['a (issue/PR reference)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.STATUS_OPEN,
pubkey,
created_at: timestamp,
content: '',
tags: [
['a', `1621:${pubkey}:issue-id`, relay]
],
id: '...',
sig: '...'
})
},
[KIND.STATUS_APPLIED]: {
...KIND_LOOKUP[KIND.STATUS_APPLIED],
writable: true,
helpText: {
description: 'Status: Applied/Merged/Resolved (NIP-34/GRASP). Marks an issue or pull request as applied, merged, or resolved.',
suggestedTags: ['a (issue/PR reference)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.STATUS_APPLIED,
pubkey,
created_at: timestamp,
content: '',
tags: [
['a', `1621:${pubkey}:issue-id`, relay]
],
id: '...',
sig: '...'
})
},
[KIND.STATUS_CLOSED]: {
...KIND_LOOKUP[KIND.STATUS_CLOSED],
writable: true,
helpText: {
description: 'Status: Closed (NIP-34/GRASP). Marks an issue or pull request as closed.',
suggestedTags: ['a (issue/PR reference)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.STATUS_CLOSED,
pubkey,
created_at: timestamp,
content: '',
tags: [
['a', `1621:${pubkey}:issue-id`, relay]
],
id: '...',
sig: '...'
})
},
[KIND.STATUS_DRAFT]: {
...KIND_LOOKUP[KIND.STATUS_DRAFT],
writable: true,
helpText: {
description: 'Status: Draft (NIP-34/GRASP). Marks an issue or pull request as draft.',
suggestedTags: ['a (issue/PR reference)']
},
exampleJSON: (pubkey, eventId, relay, timestamp) => ({
kind: KIND.STATUS_DRAFT,
pubkey,
created_at: timestamp,
content: '',
tags: [
['a', `1621:${pubkey}:issue-id`, relay]
],
id: '...',
sig: '...'
})
},
};
/**
@ -409,7 +1079,8 @@ export function getKindMetadata(kind: number): KindMetadata { @@ -409,7 +1079,8 @@ export function getKindMetadata(kind: number): KindMetadata {
* Get all writable kinds for the form
*/
export function getWritableKinds(): Array<{ value: number; label: string }> {
const writableKinds = WRITABLE_KINDS.map(kind => {
const allKinds = getAllKinds();
const writableKinds = allKinds.map((kind: number) => {
const metadata = KIND_METADATA[kind];
return {
value: kind,

Loading…
Cancel
Save