@ -18,7 +18,13 @@ import Highlight from '@/components/Note/Highlight'
import MarkdownArticle from '@/components/Note/MarkdownArticle/MarkdownArticle'
import MarkdownArticle from '@/components/Note/MarkdownArticle/MarkdownArticle'
import AsciidocArticle from '@/components/Note/AsciidocArticle/AsciidocArticle'
import AsciidocArticle from '@/components/Note/AsciidocArticle/AsciidocArticle'
import ClientTag from '@/components/ClientTag'
import ClientTag from '@/components/ClientTag'
import { ExtendedKind } from '@/constants'
import {
ExtendedKind ,
MAX_SIGNED_CUSTOM_EVENT_KIND ,
UNSIGNED_EXPERIMENTAL_KIND_MAX ,
UNSIGNED_EXPERIMENTAL_KIND_MIN ,
isUnsignedExperimentalKind
} from '@/constants'
import { applyImwaldAttributionTags } from '@/lib/draft-event'
import { applyImwaldAttributionTags } from '@/lib/draft-event'
import { createFakeEvent } from '@/lib/event'
import { createFakeEvent } from '@/lib/event'
import logger from '@/lib/logger'
import logger from '@/lib/logger'
@ -32,7 +38,7 @@ import { useNostr } from '@/providers/NostrProvider'
import storage from '@/services/local-storage.service'
import storage from '@/services/local-storage.service'
import type { TDraftEvent } from '@/types'
import type { TDraftEvent } from '@/types'
import dayjs from 'dayjs'
import dayjs from 'dayjs'
import { Plus , Trash2 } from 'lucide-react'
import { AlertTriangle , Plus , Trash2 } from 'lucide-react'
import { Event , kinds } from 'nostr-tools'
import { Event , kinds } from 'nostr-tools'
import { useCallback , useEffect , useMemo , useRef , useState , type ReactNode } from 'react'
import { useCallback , useEffect , useMemo , useRef , useState , type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from 'react-i18next'
@ -54,15 +60,15 @@ function tagsFromRows(rows: string[][]): string[][] {
return out
return out
}
}
const MAX_CUSTOM_EVENT_KIND = 40000
/** Integer kind in [0, MAX_SIGNED_CUSTOM_EVENT_KIND] or unsigned experimental range; null if invalid / empty. */
/** Integer kind in [0, 40000], or null if invalid / empty. */
function parseEventKindInput ( s : string ) : number | null {
function parseEventKindInput ( s : string ) : number | null {
const trimmed = s . trim ( )
const trimmed = s . trim ( )
if ( trimmed === '' ) return null
if ( trimmed === '' ) return null
const n = Number ( trimmed )
const n = Number ( trimmed )
if ( ! Number . isInteger ( n ) || n < 0 || n > MAX_CUSTOM_EVENT_KIND ) return null
if ( ! Number . isInteger ( n ) || n < 0 ) return null
return n
if ( n <= MAX_SIGNED_CUSTOM_EVENT_KIND ) return n
if ( isUnsignedExperimentalKind ( n ) ) return n
return null
}
}
function StaticEventPreview ( { event , className } : { event : Event ; className? : string } ) {
function StaticEventPreview ( { event , className } : { event : Event ; className? : string } ) {
@ -179,7 +185,14 @@ export default function EditOrCloneEventDialog(props: EditOrCloneEventDialogProp
const buildDraftJson = useCallback ( ( ) = > {
const buildDraftJson = useCallback ( ( ) = > {
if ( isCreate && parsedCreateKind === null ) {
if ( isCreate && parsedCreateKind === null ) {
return t ( 'Enter a valid event kind (integer 0–40000).' )
return t (
'Enter a valid event kind: integer 0–{{maxSigned}}, or {{unsignedMin}}–{{unsignedMax}} (unsigned experiment).' ,
{
maxSigned : MAX_SIGNED_CUSTOM_EVENT_KIND ,
unsignedMin : UNSIGNED_EXPERIMENTAL_KIND_MIN ,
unsignedMax : UNSIGNED_EXPERIMENTAL_KIND_MAX
}
)
}
}
const k = isCreate ? parsedCreateKind ! : sourceEvent ! . kind
const k = isCreate ? parsedCreateKind ! : sourceEvent ! . kind
const base : TDraftEvent = {
const base : TDraftEvent = {
@ -191,13 +204,18 @@ export default function EditOrCloneEventDialog(props: EditOrCloneEventDialogProp
const withAttribution = applyImwaldAttributionTags ( base , {
const withAttribution = applyImwaldAttributionTags ( base , {
addClientTag : storage.getAddClientTag ( )
addClientTag : storage.getAddClientTag ( )
} )
} )
const unsignedNote = isUnsignedExperimentalKind ( withAttribution . kind )
? t (
'Unsigned experimental kind: `sig` will be empty at publish; `id` is still the standard event hash. Not accepted by normal relays. Relays that allow this should authenticate you (e.g. NIP-42 AUTH) before writes.'
)
: t ( 'id and sig are assigned when you publish' )
const draft = {
const draft = {
pubkey : pubkey ? ? t ( 'Log in to publish' ) ,
pubkey : pubkey ? ? t ( 'Log in to publish' ) ,
kind : withAttribution.kind ,
kind : withAttribution.kind ,
content : withAttribution.content ,
content : withAttribution.content ,
tags : withAttribution.tags ,
tags : withAttribution.tags ,
created_at : t ( 'Set when you publish' ) ,
created_at : t ( 'Set when you publish' ) ,
_note : t ( 'id and sig are assigned when you publish' )
_note : unsignedNote
}
}
return JSON . stringify ( draft , null , 2 )
return JSON . stringify ( draft , null , 2 )
} , [ isCreate , parsedCreateKind , sourceEvent , pubkey , content , normalizedTags , t ] )
} , [ isCreate , parsedCreateKind , sourceEvent , pubkey , content , normalizedTags , t ] )
@ -242,7 +260,16 @@ export default function EditOrCloneEventDialog(props: EditOrCloneEventDialogProp
if ( isCreate ) {
if ( isCreate ) {
const k = parseEventKindInput ( createKindInput )
const k = parseEventKindInput ( createKindInput )
if ( k === null ) {
if ( k === null ) {
showPublishingError ( t ( 'Kind must be an integer from 0 to 40000.' ) )
showPublishingError (
t (
'Kind must be an integer from 0 to {{maxSigned}}, or from {{unsignedMin}} to {{unsignedMax}} (unsigned experiment).' ,
{
maxSigned : MAX_SIGNED_CUSTOM_EVENT_KIND ,
unsignedMin : UNSIGNED_EXPERIMENTAL_KIND_MIN ,
unsignedMax : UNSIGNED_EXPERIMENTAL_KIND_MAX
}
)
)
return
return
}
}
}
}
@ -339,15 +366,41 @@ export default function EditOrCloneEventDialog(props: EditOrCloneEventDialogProp
< Input
< Input
type = "number"
type = "number"
min = { 0 }
min = { 0 }
max = { MAX_CUSTOM_EVENT_KIND }
max = { UNSIGNED_EXPERIMENTAL_KIND_MAX }
step = { 1 }
step = { 1 }
value = { createKindInput }
value = { createKindInput }
onChange = { ( e ) = > setCreateKindInput ( e . target . value ) }
onChange = { ( e ) = > setCreateKindInput ( e . target . value ) }
className = "font-mono text-sm"
className = "font-mono text-sm"
/ >
/ >
< p className = "text-xs text-muted-foreground" >
< p className = "text-xs text-muted-foreground" >
{ t ( 'Integer from 0 to 40000' ) }
{ t (
'Signed: 0–{{maxSigned}}. Unsigned experiment (empty sig): {{unsignedMin}}–{{unsignedMax}}.' ,
{
maxSigned : MAX_SIGNED_CUSTOM_EVENT_KIND ,
unsignedMin : UNSIGNED_EXPERIMENTAL_KIND_MIN ,
unsignedMax : UNSIGNED_EXPERIMENTAL_KIND_MAX
}
) }
< / p >
{ parsedCreateKind !== null && isUnsignedExperimentalKind ( parsedCreateKind ) ? (
< div
role = "alert"
className = "flex gap-2 rounded-md border border-amber-500/50 bg-amber-500/10 p-3 text-sm text-amber-950 dark:text-amber-100"
>
< AlertTriangle
className = "h-5 w-5 shrink-0 text-amber-600 dark:text-amber-400"
aria - hidden
/ >
< div >
< p className = "font-medium" > { t ( 'Unsigned experimental kind' ) } < / p >
< p className = "mt-1 text-xs leading-relaxed opacity-90" >
{ t (
'This kind is published with an empty signature. Normal Nostr relays will reject it, and these events are not portable on the open network. Only use relays that explicitly support this experiment and authenticate you (for example with NIP-42 AUTH) before accepting writes.'
) }
< / p >
< / p >
< / div >
< / div >
) : null }
< / >
< / >
) : (
) : (
< Input
< Input
@ -445,7 +498,14 @@ export default function EditOrCloneEventDialog(props: EditOrCloneEventDialogProp
< / >
< / >
) : (
) : (
< p className = "text-sm text-muted-foreground" >
< p className = "text-sm text-muted-foreground" >
{ t ( 'Enter a valid event kind (integer 0–40000).' ) }
{ t (
'Enter a valid event kind: 0–{{maxSigned}}, or {{unsignedMin}}–{{unsignedMax}}.' ,
{
maxSigned : MAX_SIGNED_CUSTOM_EVENT_KIND ,
unsignedMin : UNSIGNED_EXPERIMENTAL_KIND_MIN ,
unsignedMax : UNSIGNED_EXPERIMENTAL_KIND_MAX
}
) }
< / p >
< / p >
) }
) }
< / div >
< / div >