Browse Source

group spells by author

copy spells
imwald
Silberengel 1 month ago
parent
commit
867d5f3852
  1. 6
      src/i18n/locales/de.ts
  2. 6
      src/i18n/locales/en.ts
  3. 30
      src/pages/primary/SpellsPage/CreateSpellDialog.tsx
  4. 224
      src/pages/primary/SpellsPage/index.tsx
  5. 3
      src/providers/NostrProvider/index.tsx
  6. 23
      src/services/client.service.ts

6
src/i18n/locales/de.ts

@ -629,6 +629,8 @@ export default {
'Noch keine Zaubersprüche. Lege mit dem Button oben einen an.', 'Noch keine Zaubersprüche. Lege mit dem Button oben einen an.',
'Loading spells from your relays…': 'Zaubersprüche werden von deinen Relays geladen…', 'Loading spells from your relays…': 'Zaubersprüche werden von deinen Relays geladen…',
'Select a spell…': 'Zauberspruch wählen…', 'Select a spell…': 'Zauberspruch wählen…',
'Spells from follows': 'Von Leuten, denen du folgst ({{count}})',
'Other spells': 'Weitere Zaubersprüche ({{count}})',
'View definition': 'Definition anzeigen', 'View definition': 'Definition anzeigen',
'Add to favorites': 'Zu Favoriten hinzufügen', 'Add to favorites': 'Zu Favoriten hinzufügen',
'Remove from favorites': 'Aus Favoriten entfernen', 'Remove from favorites': 'Aus Favoriten entfernen',
@ -661,6 +663,10 @@ export default {
'Spell definition': 'Zauberspruch-Definition', 'Spell definition': 'Zauberspruch-Definition',
'Spell published': 'Zauberspruch veröffentlicht', 'Spell published': 'Zauberspruch veröffentlicht',
'Edit spell': 'Zauberspruch bearbeiten', 'Edit spell': 'Zauberspruch bearbeiten',
'Clone spell': 'Zauberspruch klonen',
'Spell cloned': 'Zauberspruch geklont',
'Clone spell intro':
'Dieser Zauberspruch wurde aus der Definition eines anderen Autors übernommen. Passe alles an, dann speichern, um einen neuen Zauberspruch mit deinem Konto zu veröffentlichen.',
'Spell updated': 'Zauberspruch aktualisiert', 'Spell updated': 'Zauberspruch aktualisiert',
'Relay URL': 'Relay', 'Relay URL': 'Relay',
Count: 'Anzahl', Count: 'Anzahl',

6
src/i18n/locales/en.ts

@ -707,6 +707,8 @@ export default {
'Could not run this spell. Check that it has a valid REQ/COUNT command, or add write relays in settings.': 'Could not run this spell. Check that it has a valid REQ/COUNT command, or add write relays in settings.':
'Could not run this spell. Check that it has a valid REQ/COUNT command, or add write relays in settings.', 'Could not run this spell. Check that it has a valid REQ/COUNT command, or add write relays in settings.',
'Select a spell…': 'Select a spell…', 'Select a spell…': 'Select a spell…',
'Spells from follows': 'From people you follow ({{count}})',
'Other spells': 'Other spells ({{count}})',
'Select a spell to view its feed.': 'Select a spell to view its feed.', 'Select a spell to view its feed.': 'Select a spell to view its feed.',
'Add another row': 'Add another row', 'Add another row': 'Add another row',
'Remove this row': 'Remove this row', 'Remove this row': 'Remove this row',
@ -721,6 +723,10 @@ export default {
'Spell form fields': 'Spell form fields', 'Spell form fields': 'Spell form fields',
'Counting matching events…': 'Counting matching events…', 'Counting matching events…': 'Counting matching events…',
'Edit spell': 'Edit spell', 'Edit spell': 'Edit spell',
'Clone spell': 'Clone spell',
'Spell cloned': 'Spell cloned',
'Clone spell intro':
'This spell is preloaded from another author’s definition. Change anything you like, then save to publish a new spell signed with your account.',
'Spell updated': 'Spell updated', 'Spell updated': 'Spell updated',
'Relay URL': 'Relay', 'Relay URL': 'Relay',
Count: 'Count', Count: 'Count',

30
src/pages/primary/SpellsPage/CreateSpellDialog.tsx

@ -134,7 +134,8 @@ export default function CreateSpellDialog({
open, open,
onOpenChange, onOpenChange,
onSaved, onSaved,
spellToEdit spellToEdit,
spellToClone
}: { }: {
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
@ -142,6 +143,8 @@ export default function CreateSpellDialog({
onSaved?: (publishedEvent?: NostrEvent) => void onSaved?: (publishedEvent?: NostrEvent) => void
/** When set, form is preloaded and save replaces this spell id in storage/favorites. */ /** When set, form is preloaded and save replaces this spell id in storage/favorites. */
spellToEdit?: NostrEvent | null spellToEdit?: NostrEvent | null
/** When set, form is preloaded from this spell but save always publishes a new event (your pubkey). */
spellToClone?: NostrEvent | null
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey, publish, checkLogin } = useNostr() const { pubkey, publish, checkLogin } = useNostr()
@ -151,12 +154,13 @@ export default function CreateSpellDialog({
useEffect(() => { useEffect(() => {
if (!open) return if (!open) return
if (spellToEdit) { const source = spellToClone ?? spellToEdit
setForm(spellEventToDraftParams(spellToEdit)) if (source) {
setForm(spellEventToDraftParams(source))
} else { } else {
setForm({ ...DEFAULT_PARAMS }) setForm({ ...DEFAULT_PARAMS })
} }
}, [open, spellToEdit]) }, [open, spellToEdit, spellToClone])
const handleScrollBodyKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => { const handleScrollBodyKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
const el = scrollBodyRef.current const el = scrollBodyRef.current
@ -210,7 +214,9 @@ export default function CreateSpellDialog({
handleClear() handleClear()
onSaved?.(event) onSaved?.(event)
onOpenChange(false) onOpenChange(false)
showSimplePublishSuccess(replaceSpellId ? t('Spell updated') : t('Spell published')) showSimplePublishSuccess(
replaceSpellId ? t('Spell updated') : spellToClone ? t('Spell cloned') : t('Spell published')
)
} catch (e) { } catch (e) {
logger.error('[CreateSpellDialog] Publish failed', e) logger.error('[CreateSpellDialog] Publish failed', e)
showPublishingError(e instanceof Error ? e : new Error(String(e))) showPublishingError(e instanceof Error ? e : new Error(String(e)))
@ -238,12 +244,18 @@ export default function CreateSpellDialog({
<X className="size-4" /> <X className="size-4" />
</Button> </Button>
<DialogHeader className="space-y-1.5 pr-10 text-left sm:text-left"> <DialogHeader className="space-y-1.5 pr-10 text-left sm:text-left">
<DialogTitle>{replaceSpellId ? t('Edit spell') : t('Create a Spell')}</DialogTitle> <DialogTitle>
{replaceSpellId ? t('Edit spell') : spellToClone ? t('Clone spell') : t('Create a Spell')}
</DialogTitle>
</DialogHeader> </DialogHeader>
<p className="mt-2 text-sm text-muted-foreground"> <p className="mt-2 text-sm text-muted-foreground">
{t( {spellToClone
'Spells are saved relay filters (NIP-A7). Fill in the filter fields below. Use $me for your pubkey and $contacts for your follow list when executing.' ? t(
)} 'This spell is preloaded from someone else’s definition. Adjust anything you like, then save to publish a new spell signed by you.'
)
: t(
'Spells are saved relay filters (NIP-A7). Fill in the filter fields below. Use $me for your pubkey and $contacts for your follow list when executing.'
)}
</p> </p>
</div> </div>

224
src/pages/primary/SpellsPage/index.tsx

@ -12,15 +12,11 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger DropdownMenuTrigger
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
@ -38,17 +34,14 @@ import {
spellIsCount spellIsCount
} from '@/services/spell.service' } from '@/services/spell.service'
import { TFeedSubRequest } from '@/types' import { TFeedSubRequest } from '@/types'
import { FileText, MoreVertical, Pencil, Plus, Star, Trash2, Wand2 } from 'lucide-react' import { Check, ChevronDown, Copy, FileText, MoreVertical, Pencil, Plus, Star, Trash2, Wand2 } from 'lucide-react'
import type { Event } from 'nostr-tools' import type { Event } from 'nostr-tools'
import { verifyEvent } from 'nostr-tools' import { verifyEvent } from 'nostr-tools'
import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Fragment, forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import CreateSpellDialog from './CreateSpellDialog' import CreateSpellDialog from './CreateSpellDialog'
import type { TPageRef } from '@/types' import type { TPageRef } from '@/types'
/** Sentinel value for Radix Select when no spell is selected */
const SPELL_SELECT_NONE = '__spell_none__'
const SpellsPage = forwardRef<TPageRef>(function SpellsPage(_, ref) { const SpellsPage = forwardRef<TPageRef>(function SpellsPage(_, ref) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey, relayList } = useNostr() const { pubkey, relayList } = useNostr()
@ -57,6 +50,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(_, ref) {
const [selectedSpell, setSelectedSpell] = useState<Event | null>(null) const [selectedSpell, setSelectedSpell] = useState<Event | null>(null)
const [createOpen, setCreateOpen] = useState(false) const [createOpen, setCreateOpen] = useState(false)
const [spellToEdit, setSpellToEdit] = useState<Event | null>(null) const [spellToEdit, setSpellToEdit] = useState<Event | null>(null)
const [spellToClone, setSpellToClone] = useState<Event | null>(null)
const [definitionSpell, setDefinitionSpell] = useState<Event | null>(null) const [definitionSpell, setDefinitionSpell] = useState<Event | null>(null)
const [subRequests, setSubRequests] = useState<TFeedSubRequest[]>([]) const [subRequests, setSubRequests] = useState<TFeedSubRequest[]>([])
const [contacts, setContacts] = useState<string[]>([]) const [contacts, setContacts] = useState<string[]>([])
@ -355,13 +349,51 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(_, ref) {
[loadSpells, selectedSpell?.id] [loadSpells, selectedSpell?.id]
) )
const orderedSpells = [...spells].sort((a, b) => { const { ownSpells, followSpells, otherSpells, spellsForSelect } = useMemo(() => {
const aFav = favoriteIds.has(a.id) const byName = (a: Event, b: Event) =>
const bFav = favoriteIds.has(b.id) getSpellName(a).localeCompare(getSpellName(b), undefined, { sensitivity: 'base' })
if (aFav && !bFav) return -1
if (!aFav && bFav) return 1 const followSet = new Set(contacts)
return (b.created_at ?? 0) - (a.created_at ?? 0) const own: Event[] = []
}) const follow: Event[] = []
const other: Event[] = []
for (const s of spells) {
if (pubkey && s.pubkey === pubkey) own.push(s)
else if (followSet.has(s.pubkey)) follow.push(s)
else other.push(s)
}
own.sort(byName)
follow.sort(byName)
other.sort(byName)
return {
ownSpells: own,
followSpells: follow,
otherSpells: other,
spellsForSelect: [...own, ...follow, ...other]
}
}, [spells, pubkey, contacts])
const spellMenuLabel = useCallback(
(spell: Event) => (favoriteIds.has(spell.id) ? `${getSpellName(spell)}` : getSpellName(spell)),
[favoriteIds]
)
const renderSpellMenuItem = useCallback(
(spell: Event) => (
<DropdownMenuItem onSelect={() => setSelectedSpell(spell)} className="gap-2">
<span className="flex size-4 shrink-0 items-center justify-center">
{selectedSpell?.id === spell.id ? <Check className="size-4" aria-hidden /> : null}
</span>
<span className="min-w-0 truncate">{spellMenuLabel(spell)}</span>
</DropdownMenuItem>
),
[selectedSpell?.id, spellMenuLabel]
)
const selectedSpellIsOwn = !!(pubkey && selectedSpell && selectedSpell.pubkey === pubkey)
return ( return (
<PrimaryPageLayout <PrimaryPageLayout
@ -375,6 +407,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(_, ref) {
size="titlebar-icon" size="titlebar-icon"
onClick={() => { onClick={() => {
setSpellToEdit(null) setSpellToEdit(null)
setSpellToClone(null)
setCreateOpen(true) setCreateOpen(true)
}} }}
title={t('Create a Spell')} title={t('Create a Spell')}
@ -388,26 +421,74 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(_, ref) {
<div className="flex min-h-0 flex-1 flex-col gap-4 p-4"> <div className="flex min-h-0 flex-1 flex-col gap-4 p-4">
{/* Spell picker + actions above the feed */} {/* Spell picker + actions above the feed */}
<div className="flex shrink-0 flex-col gap-2 sm:flex-row sm:items-center sm:gap-3"> <div className="flex shrink-0 flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
<Select <DropdownMenu>
value={selectedSpell?.id ?? SPELL_SELECT_NONE} <DropdownMenuTrigger asChild>
onValueChange={(v) => { <Button
if (v === SPELL_SELECT_NONE) setSelectedSpell(null) variant="outline"
else setSelectedSpell(orderedSpells.find((s) => s.id === v) ?? null) disabled={spellsForSelect.length === 0}
}} className="min-w-0 flex-1 justify-between font-normal sm:max-w-md"
disabled={orderedSpells.length === 0} title={selectedSpell ? spellMenuLabel(selectedSpell) : undefined}
> >
<SelectTrigger className="min-w-0 flex-1 sm:max-w-md"> <span className="truncate">
<SelectValue placeholder={t('Select a spell…')} /> {selectedSpell ? spellMenuLabel(selectedSpell) : t('Select a spell…')}
</SelectTrigger> </span>
<SelectContent> <ChevronDown className="ml-2 size-4 shrink-0 opacity-50" aria-hidden />
<SelectItem value={SPELL_SELECT_NONE}>{t('Select a spell…')}</SelectItem> </Button>
{orderedSpells.map((spell) => ( </DropdownMenuTrigger>
<SelectItem key={spell.id} value={spell.id}> <DropdownMenuContent
{favoriteIds.has(spell.id) ? `${getSpellName(spell)}` : getSpellName(spell)} align="start"
</SelectItem> className="max-h-[min(24rem,70vh)] w-[var(--radix-dropdown-menu-trigger-width)] min-w-[12rem] overflow-y-auto sm:max-w-md"
>
<DropdownMenuItem onSelect={() => setSelectedSpell(null)} className="gap-2">
<span className="flex size-4 shrink-0 items-center justify-center">
{!selectedSpell ? <Check className="size-4" aria-hidden /> : null}
</span>
<span className="truncate">{t('Select a spell…')}</span>
</DropdownMenuItem>
{(ownSpells.length > 0 || followSpells.length > 0 || otherSpells.length > 0) && (
<DropdownMenuSeparator />
)}
{ownSpells.map((spell) => (
<Fragment key={spell.id}>{renderSpellMenuItem(spell)}</Fragment>
))} ))}
</SelectContent> {ownSpells.length > 0 && (followSpells.length > 0 || otherSpells.length > 0) && (
</Select> <DropdownMenuSeparator />
)}
{followSpells.length > 0 ? (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="cursor-default">
{t('Spells from follows', { count: followSpells.length })}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
className="max-h-[50vh] min-w-[12rem] overflow-y-auto sm:min-w-[16rem]"
showScrollButtons
>
{followSpells.map((spell) => (
<Fragment key={spell.id}>{renderSpellMenuItem(spell)}</Fragment>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
) : null}
{otherSpells.length > 0 && (ownSpells.length > 0 || followSpells.length > 0) ? (
<DropdownMenuSeparator />
) : null}
{otherSpells.length > 0 ? (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="cursor-default">
{t('Other spells', { count: otherSpells.length })}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
className="max-h-[50vh] min-w-[12rem] overflow-y-auto sm:min-w-[16rem]"
showScrollButtons
>
{otherSpells.map((spell) => (
<Fragment key={spell.id}>{renderSpellMenuItem(spell)}</Fragment>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
) : null}
</DropdownMenuContent>
</DropdownMenu>
<div className="flex shrink-0 flex-wrap items-center gap-2"> <div className="flex shrink-0 flex-wrap items-center gap-2">
<Button <Button
@ -415,6 +496,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(_, ref) {
variant="outline" variant="outline"
onClick={() => { onClick={() => {
setSpellToEdit(null) setSpellToEdit(null)
setSpellToClone(null)
setCreateOpen(true) setCreateOpen(true)
}} }}
> >
@ -445,28 +527,47 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(_, ref) {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem {selectedSpellIsOwn ? (
className="gap-2" <DropdownMenuItem
onClick={() => { className="gap-2"
setSpellToEdit(selectedSpell) onClick={() => {
setCreateOpen(true) setSpellToClone(null)
}} setSpellToEdit(selectedSpell)
> setCreateOpen(true)
<Pencil className="size-4" /> }}
{t('Edit spell')} >
</DropdownMenuItem> <Pencil className="size-4" />
{t('Edit spell')}
</DropdownMenuItem>
) : (
<DropdownMenuItem
className="gap-2"
onClick={() => {
setSpellToEdit(null)
setSpellToClone(selectedSpell)
setCreateOpen(true)
}}
>
<Copy className="size-4" />
{t('Clone spell')}
</DropdownMenuItem>
)}
<DropdownMenuItem className="gap-2" onClick={() => setDefinitionSpell(selectedSpell)}> <DropdownMenuItem className="gap-2" onClick={() => setDefinitionSpell(selectedSpell)}>
<FileText className="size-4" /> <FileText className="size-4" />
{t('View definition')} {t('View definition')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> {selectedSpellIsOwn ? (
<DropdownMenuItem <>
className="gap-2 text-destructive focus:text-destructive" <DropdownMenuSeparator />
onClick={() => handleDeleteSpell(selectedSpell)} <DropdownMenuItem
> className="gap-2 text-destructive focus:text-destructive"
<Trash2 className="size-4" /> onClick={() => handleDeleteSpell(selectedSpell)}
{t('Delete')} >
</DropdownMenuItem> <Trash2 className="size-4" />
{t('Delete')}
</DropdownMenuItem>
</>
) : null}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</> </>
@ -478,7 +579,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(_, ref) {
<p className="text-xs text-muted-foreground">{t('Loading spells from your relays…')}</p> <p className="text-xs text-muted-foreground">{t('Loading spells from your relays…')}</p>
) : null} ) : null}
{orderedSpells.length === 0 && !spellsCatalogSyncing && ( {spellsForSelect.length === 0 && !spellsCatalogSyncing && (
<p className="text-sm text-muted-foreground">{t('No spells yet. Create one with the button above.')}</p> <p className="text-sm text-muted-foreground">{t('No spells yet. Create one with the button above.')}</p>
)} )}
@ -605,14 +706,21 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(_, ref) {
open={createOpen} open={createOpen}
onOpenChange={(open) => { onOpenChange={(open) => {
setCreateOpen(open) setCreateOpen(open)
if (!open) setSpellToEdit(null) if (!open) {
setSpellToEdit(null)
setSpellToClone(null)
}
}} }}
spellToEdit={spellToEdit} spellToEdit={spellToEdit}
spellToClone={spellToClone}
onSaved={(ev) => { onSaved={(ev) => {
void loadSpells() void loadSpells()
if (ev && spellToEdit && selectedSpell?.id === spellToEdit.id) { if (ev && spellToEdit && selectedSpell?.id === spellToEdit.id) {
setSelectedSpell(ev) setSelectedSpell(ev)
} }
if (ev && spellToClone && selectedSpell?.id === spellToClone.id) {
setSelectedSpell(ev)
}
}} }}
/> />

3
src/providers/NostrProvider/index.tsx

@ -551,7 +551,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
} else { } else {
client.signer = undefined client.signer = undefined
} }
}, [signer]) client.signerType = account?.signerType
}, [signer, account?.signerType])
useEffect(() => { useEffect(() => {
if (account) { if (account) {

23
src/services/client.service.ts

@ -13,7 +13,15 @@ import { formatPubkey, isValidPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib
import { getPubkeysFromPTags, getServersFromServerTags, tagNameEquals } from '@/lib/tag' import { getPubkeysFromPTags, getServersFromServerTags, tagNameEquals } from '@/lib/tag'
import { isLocalNetworkUrl, isWebsocketUrl, normalizeUrl, simplifyUrl } from '@/lib/url' import { isLocalNetworkUrl, isWebsocketUrl, normalizeUrl, simplifyUrl } from '@/lib/url'
import { isSafari } from '@/lib/utils' import { isSafari } from '@/lib/utils'
import { ISigner, TProfile, TPublishOptions, TRelayList, TMailboxRelay, TSubRequestFilter } from '@/types' import {
ISigner,
TProfile,
TPublishOptions,
TRelayList,
TMailboxRelay,
TSignerType,
TSubRequestFilter
} from '@/types'
import { sha256 } from '@noble/hashes/sha2' import { sha256 } from '@noble/hashes/sha2'
import DataLoader from 'dataloader' import DataLoader from 'dataloader'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@ -40,6 +48,8 @@ class ClientService extends EventTarget {
static instance: ClientService static instance: ClientService
signer?: ISigner signer?: ISigner
/** Set with signer from NostrProvider; used to skip relay AUTH when read-only (e.g. npub). */
signerType?: TSignerType
pubkey?: string pubkey?: string
private pool: SimplePool private pool: SimplePool
@ -186,6 +196,13 @@ class ClientService extends EventTarget {
} }
} }
/** Read-only logins (e.g. npub) cannot sign relay AUTH challenges; avoid calling signEvent. */
private canSignerAuthenticateRelay(): boolean {
if (!this.signer) return false
if (this.signerType === 'npub') return false
return true
}
/** /**
* Determine which relays to publish an event to. * Determine which relays to publish an event to.
* Fallbacks (used when user relay list is empty or fetch fails): * Fallbacks (used when user relay list is empty or fetch fails):
@ -684,7 +701,7 @@ class ClientService extends EventTarget {
if ( if (
error instanceof Error && error instanceof Error &&
error.message.startsWith('auth-required') && error.message.startsWith('auth-required') &&
!!that.signer that.canSignerAuthenticateRelay()
) { ) {
logger.debug(`[PublishEvent] Auth required, attempting authentication`, { url }) logger.debug(`[PublishEvent] Auth required, attempting authentication`, { url })
return relay return relay
@ -1048,7 +1065,7 @@ class ClientService extends EventTarget {
oneose: () => handleEose(i), oneose: () => handleEose(i),
onclose: (reason: string) => { onclose: (reason: string) => {
releaseOnce() releaseOnce()
if (reason.startsWith('auth-required: ') && that.signer) { if (reason.startsWith('auth-required: ') && that.canSignerAuthenticateRelay()) {
relay.auth(async (authEvt: EventTemplate) => { relay.auth(async (authEvt: EventTemplate) => {
const evt = await that.signer!.signEvent(authEvt) const evt = await that.signer!.signEvent(authEvt)
if (!evt) throw new Error('sign event failed') if (!evt) throw new Error('sign event failed')

Loading…
Cancel
Save