Browse Source

populate a spell from a list

imwald
Silberengel 1 month ago
parent
commit
b9960036c3
  1. 24
      src/i18n/locales/de.ts
  2. 23
      src/i18n/locales/en.ts
  3. 25
      src/lib/event.ts
  4. 198
      src/lib/spell-list-import.ts
  5. 209
      src/pages/primary/SpellsPage/CreateSpellDialog.tsx
  6. 8
      src/services/indexed-db.service.ts

24
src/i18n/locales/de.ts

@ -712,6 +712,30 @@ export default {
'Saving…': 'Speichern…', 'Saving…': 'Speichern…',
Clear: 'Leeren', Clear: 'Leeren',
listImportManualLabel: 'Aus Event vorausfüllen',
listImportFromEventHint:
'Unterstützte Tags werden ins Formular übernommen (e, p, t, relay, r, a wo möglich). Nicht leerer Inhalt wird übersprungen; verschlüsselte private Einträge werden nicht gelesen. Es können Hinweise zu nicht abgebildeten Tags erscheinen.',
listImportManualPlaceholder: '64 Zeichen Hex, nevent1… oder naddr1…',
listImportLoadManual: 'Anwenden',
listImportContentSkipped:
'Dieses Event hat nicht leeren Inhalt (evtl. verschlüsselte private Einträge). Es wurden nur öffentliche Tags übernommen.',
listImportUnsupportedEmoji:
'Diese Liste enthält Emoji-Tags (NIP-30); die werden nicht in den Zauber-Filter übernommen.',
listImportUnsupportedTag:
'Tags vom Typ „{{tag}}“ ({{count}}) werden noch nicht auf Zauber-Filter abgebildet.',
listImportBadATag: 'Adress-Tag nicht lesbar: {{preview}}…',
listImportATagNotFound: 'Adress-Tag nicht gefunden: {{preview}}…',
listImportATagFailed: 'Adress-Tag konnte nicht aufgelöst werden: {{preview}}…',
listImportEventNotFound: 'Kein Event zu dieser Referenz gefunden.',
'REQ tag filters': 'REQ-Tag-Filter',
spellTagFiltersHint:
'Optionale Filter auf abonnierte Events (NIP-01 Ein-Buchstaben-Tags). Beispiel: Buchstabe „t“, Werte „bitcoin“.',
spellTagFiltersEmpty:
'Noch keine Tag-Filter. Zeilen unten hinzufügen oder oben eine Event-Referenz anwenden.',
'Tag filter letter': 'Tag-Buchstabe',
'Filter value': 'Wert',
'Add tag filter': 'Tag-Filter hinzufügen',
'doublePane.secondaryEmpty': 'doublePane.secondaryEmpty':
'Öffne eine Notiz, ein Profil oder Einstellungen, um sie hier anzuzeigen.', 'Öffne eine Notiz, ein Profil oder Einstellungen, um sie hier anzuzeigen.',
'doublePane.secondaryEmptyHint': 'Feed und Hauptseiten bleiben links.' 'doublePane.secondaryEmptyHint': 'Feed und Hauptseiten bleiben links.'

23
src/i18n/locales/en.ts

@ -747,6 +747,29 @@ export default {
'REQ returns a feed; COUNT returns a number.': 'REQ returns a feed; COUNT returns a number.':
'REQ: scrollable feed (choose live Feed or one-shot Fetch below). COUNT: a single number, no feed.', 'REQ: scrollable feed (choose live Feed or one-shot Fetch below). COUNT: a single number, no feed.',
listImportManualLabel: 'Pre-fill from event',
listImportFromEventHint:
'Supported tags are merged into the form (e, p, t, relay, r, a where possible). Non-empty content is skipped; encrypted private items are not read. You may see notices for unmapped tags.',
listImportManualPlaceholder: '64-char hex, nevent1…, or naddr1…',
listImportLoadManual: 'Apply',
listImportContentSkipped:
'This event has non-empty content (may include encrypted private items). Only public tags were merged.',
listImportUnsupportedEmoji:
'This list includes emoji tags (NIP-30); those are not added to the spell filter.',
listImportUnsupportedTag:
'Tags of type “{{tag}}” ({{count}}) are not mapped to spell filters yet.',
listImportBadATag: 'Could not parse address tag: {{preview}}…',
listImportATagNotFound: 'Could not resolve address tag: {{preview}}…',
listImportATagFailed: 'Failed to resolve address tag: {{preview}}…',
listImportEventNotFound: 'No event found for that reference.',
'REQ tag filters': 'REQ tag filters',
spellTagFiltersHint:
'Optional filters on subscribed events (NIP-01 single-letter tags). Example: letter “t”, values “bitcoin”.',
spellTagFiltersEmpty: 'No tag filters yet. Add rows below or apply an event reference above.',
'Tag filter letter': 'Tag letter',
'Filter value': 'Value',
'Add tag filter': 'Add tag filter',
Spells: 'Spells', Spells: 'Spells',
'doublePane.secondaryEmpty': 'Open a note, profile, or settings item to show it here.', 'doublePane.secondaryEmpty': 'Open a note, profile, or settings item to show it here.',

25
src/lib/event.ts

@ -185,7 +185,7 @@ export function getReplaceableCoordinate(kind: number, pubkey: string, d: string
} }
export function getReplaceableCoordinateFromEvent(event: Event) { export function getReplaceableCoordinateFromEvent(event: Event) {
const d = event.tags.find(tagNameEquals('d'))?.[1] const d = event.tags.find(tagNameEquals('d'))?.[1] ?? ''
return getReplaceableCoordinate(event.kind, event.pubkey, d) return getReplaceableCoordinate(event.kind, event.pubkey, d)
} }
@ -346,3 +346,26 @@ export function getRetainedEvent(a: Event, b: Event): Event {
} }
return b return b
} }
/**
* Collapse replaceable/addressable events to one per NIP-01 coordinate (`kind:pubkey` or `kind:pubkey:d`),
* keeping the newest (`created_at`, then lexicographically smallest `id` on ties).
* Non-replaceable events are keyed by `id` only.
*/
export function dedupeToLatestPerReplaceableCoordinate(events: Event[]): Event[] {
const byKey = new Map<string, Event>()
for (const e of events) {
if (!isReplaceableEvent(e.kind)) {
byKey.set(e.id, e)
continue
}
const coord = getReplaceableCoordinateFromEvent(e)
const existing = byKey.get(coord)
if (!existing) {
byKey.set(coord, e)
continue
}
byKey.set(coord, getRetainedEvent(e, existing))
}
return [...byKey.values()]
}

198
src/lib/spell-list-import.ts

@ -0,0 +1,198 @@
/**
* Merge tags from any fetched Nostr event into NIP-A7 spell draft fields (best-effort).
*/
import type { TSpellDraftParams } from '@/lib/draft-event'
import { isValidPubkey } from '@/lib/pubkey'
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
import type { Event } from 'nostr-tools'
import type { Filter } from 'nostr-tools'
import client from '@/services/client.service'
const HEX64 = /^[0-9a-f]{64}$/i
/** Metadata tags on list events — not mapped to spell filters. */
const LIST_METADATA_TAGS = new Set([
'd',
'title',
'image',
'description',
'client',
'alt',
'expiration',
'relay' // handled separately below
])
/** Tags we explicitly report as unsupported for spell import. */
const KNOWN_UNSUPPORTED = new Set(['emoji', 'word', 'group'])
export function dedupeAppendIds(base: string[], add: string[]): string[] {
const seen = new Set(
base
.map((s) => s.trim().toLowerCase())
.filter(Boolean)
)
const out = base.map((s) => s.trim()).filter(Boolean)
for (const raw of add) {
const t = raw.trim()
if (!t) continue
const k = t.toLowerCase()
if (seen.has(k)) continue
seen.add(k)
out.push(t)
}
return out
}
function mergeTagLetter(
rows: { letter: string; values: string[] }[],
letter: string,
values: string[]
): { letter: string; values: string[] }[] {
const vset = new Set(values.map((v) => v.trim()).filter(Boolean))
if (vset.size === 0) return rows
const mergedVals = [...vset]
const idx = rows.findIndex((r) => r.letter === letter)
if (idx < 0) return [...rows, { letter, values: mergedVals }]
const prev = rows[idx]!
const u = new Set([...prev.values.map((x) => x.trim()).filter(Boolean), ...mergedVals])
return rows.map((r, i) => (i === idx ? { letter, values: [...u] } : r))
}
export type TListToSpellResult = {
draft: TSpellDraftParams
notices: string[]
/** `a` coordinates to resolve to event ids in the background */
pendingATags: string[]
}
/**
* Merge public tags from a list/set event into spell draft fields.
* Does not resolve `a` tags use {@link resolveSpellListATags} after.
*/
export function applyListEventToSpellDraft(
base: TSpellDraftParams,
listEvent: Event
): TListToSpellResult {
const notices: string[] = []
const pendingATags: string[] = []
const unsupportedCounts = new Map<string, number>()
let draft: TSpellDraftParams = {
...base,
ids: [...base.ids],
authors: [...base.authors],
relays: [...base.relays],
topics: [...base.topics],
tagFilters: base.tagFilters.map((r) => ({ letter: r.letter, values: [...r.values] })),
kinds: [...base.kinds]
}
if ((listEvent.content ?? '').trim().length > 0) {
notices.push('listImportContentSkipped')
}
const title = listEvent.tags.find((t) => t[0] === 'title')?.[1]?.trim()
if (title && !(draft.name ?? '').trim()) {
draft = { ...draft, name: title }
}
for (const tag of listEvent.tags) {
const name = tag[0]
if (!name) continue
if (LIST_METADATA_TAGS.has(name) && name !== 'relay') continue
if (name === 't' && tag[1]) {
const v = tag[1].trim()
if (v) draft.tagFilters = mergeTagLetter(draft.tagFilters, 't', [v])
continue
}
if (name === 'e' && tag[1] && HEX64.test(tag[1])) {
draft.ids = dedupeAppendIds(draft.ids, [tag[1]])
continue
}
if (name === 'p' && tag[1] && isValidPubkey(tag[1])) {
draft.authors = dedupeAppendIds(draft.authors, [tag[1]])
continue
}
if (name === 'relay' && tag[1]) {
const u = normalizeUrl(tag[1]) || tag[1]
if (isWebsocketUrl(u)) draft.relays = dedupeAppendIds(draft.relays, [u])
continue
}
if (name === 'r' && tag[1]) {
const u = normalizeUrl(tag[1]) || tag[1]
if (isWebsocketUrl(u)) draft.relays = dedupeAppendIds(draft.relays, [u])
continue
}
if (name === 'a' && tag[1]) {
pendingATags.push(tag[1])
continue
}
if (KNOWN_UNSUPPORTED.has(name)) {
unsupportedCounts.set(name, (unsupportedCounts.get(name) ?? 0) + 1)
continue
}
if (LIST_METADATA_TAGS.has(name)) continue
unsupportedCounts.set(name, (unsupportedCounts.get(name) ?? 0) + 1)
}
for (const [n, c] of unsupportedCounts) {
if (n === 'emoji') notices.push('listImportUnsupportedEmoji')
else notices.push(`listImportUnsupportedTag:${n}:${c}`)
}
return { draft, notices, pendingATags: [...new Set(pendingATags)] }
}
/** Resolve NIP-33 address strings (`kind:pubkey:d…`) to latest replaceable event ids. */
export async function resolveSpellListATags(
aTags: string[],
relayUrls: string[]
): Promise<{ ids: string[]; notices: string[] }> {
const ids: string[] = []
const notices: string[] = []
const relays = relayUrls.length ? relayUrls : []
await Promise.all(
aTags.map(async (at) => {
const parts = at.split(':')
if (parts.length < 3) {
notices.push(`listImportBadATag:${at.slice(0, 32)}`)
return
}
const kind = parseInt(parts[0]!, 10)
const author = parts[1]!
const d = parts.slice(2).join(':')
if (Number.isNaN(kind) || !isValidPubkey(author) || !d) {
notices.push(`listImportBadATag:${at.slice(0, 32)}`)
return
}
const filter: Filter = { kinds: [kind], authors: [author], '#d': [d], limit: 5 }
try {
const events =
relays.length > 0
? await client.fetchEvents(relays, filter, { globalTimeout: 12_000 })
: await client.fetchEvents([], filter, { globalTimeout: 12_000 })
if (!events.length) {
notices.push(`listImportATagNotFound:${at.slice(0, 48)}`)
return
}
const latest = [...events].sort((a, b) => b.created_at - a.created_at)[0]!
ids.push(latest.id)
} catch {
notices.push(`listImportATagFailed:${at.slice(0, 48)}`)
}
})
)
return { ids: [...new Set(ids)], notices }
}

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

@ -13,9 +13,16 @@ import {
spellEventToDraftParams, spellEventToDraftParams,
type TSpellDraftParams type TSpellDraftParams
} from '@/lib/draft-event' } from '@/lib/draft-event'
import {
applyListEventToSpellDraft,
dedupeAppendIds,
resolveSpellListATags
} from '@/lib/spell-list-import'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { showPublishingError, showSimplePublishSuccess } from '@/lib/publishing-feedback' import { showPublishingError, showSimplePublishSuccess } from '@/lib/publishing-feedback'
import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import { getRelaysForSpellCatalogSync } from '@/services/spell.service'
import { Minus, Plus, X } from 'lucide-react' import { Minus, Plus, X } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { Event as NostrEvent } from 'nostr-tools' import type { Event as NostrEvent } from 'nostr-tools'
@ -61,7 +68,8 @@ function DynamicStringListField({
values, values,
onChange, onChange,
placeholder, placeholder,
inputType = 'text' inputType = 'text',
showLabel = true
}: { }: {
label: string label: string
hint?: string hint?: string
@ -69,6 +77,8 @@ function DynamicStringListField({
onChange: (next: string[]) => void onChange: (next: string[]) => void
placeholder?: string placeholder?: string
inputType?: 'text' | 'number' inputType?: 'text' | 'number'
/** When false, only the inputs and add/remove controls are rendered (for nested editors). */
showLabel?: boolean
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const rows = values.length > 0 ? values : [''] const rows = values.length > 0 ? values : ['']
@ -96,7 +106,7 @@ function DynamicStringListField({
return ( return (
<div className="grid gap-2"> <div className="grid gap-2">
<Label>{label}</Label> {showLabel && label ? <Label>{label}</Label> : null}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{rows.map((v, i) => ( {rows.map((v, i) => (
<div key={i} className="flex gap-2"> <div key={i} className="flex gap-2">
@ -130,6 +140,99 @@ function DynamicStringListField({
) )
} }
function TagFiltersEditor({
tagFilters,
onChange
}: {
tagFilters: { letter: string; values: string[] }[]
onChange: (next: { letter: string; values: string[] }[]) => void
}) {
const { t } = useTranslation()
const addRow = () => onChange([...tagFilters, { letter: '', values: [''] }])
const removeRow = (i: number) => {
const next = [...tagFilters]
next.splice(i, 1)
onChange(next)
}
return (
<div className="grid gap-2">
<Label>{t('REQ tag filters')}</Label>
<p className="text-xs text-muted-foreground">{t('spellTagFiltersHint')}</p>
{tagFilters.length === 0 ? (
<p className="text-xs text-muted-foreground">{t('spellTagFiltersEmpty')}</p>
) : null}
{tagFilters.map((row, i) => (
<div key={i} className="flex flex-col gap-2 rounded-md border border-border p-3">
<div className="flex items-center gap-2">
<Input
className="h-9 w-16 font-mono text-sm uppercase"
placeholder="t"
value={row.letter}
maxLength={8}
onChange={(e) => {
const next = [...tagFilters]
next[i] = { ...next[i]!, letter: e.target.value }
onChange(next)
}}
aria-label={t('Tag filter letter')}
/>
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9 shrink-0"
onClick={() => removeRow(i)}
title={t('Remove this row')}
aria-label={t('Remove this row')}
>
<Minus className="size-4" />
</Button>
</div>
<DynamicStringListField
label=""
showLabel={false}
values={row.values.length > 0 ? row.values : ['']}
onChange={(values) => {
const next = [...tagFilters]
next[i] = { ...next[i]!, values }
onChange(next)
}}
placeholder={t('Filter value')}
/>
</div>
))}
<Button type="button" variant="outline" size="sm" className="h-9 w-fit gap-1" onClick={addRow}>
<Plus className="size-4" />
{t('Add tag filter')}
</Button>
</div>
)
}
function formatListImportNotice(raw: string, t: (k: string, o?: Record<string, unknown>) => string) {
if (raw === 'listImportContentSkipped') return t('listImportContentSkipped')
if (raw === 'listImportUnsupportedEmoji') return t('listImportUnsupportedEmoji')
if (raw.startsWith('listImportUnsupportedTag:')) {
const parts = raw.split(':')
const tag = parts[1] ?? '?'
const count = parts[2] ?? '1'
return t('listImportUnsupportedTag', { tag, count })
}
if (raw.startsWith('listImportBadATag:')) {
const preview = raw.slice('listImportBadATag:'.length)
return t('listImportBadATag', { preview })
}
if (raw.startsWith('listImportATagNotFound:')) {
const preview = raw.slice('listImportATagNotFound:'.length)
return t('listImportATagNotFound', { preview })
}
if (raw.startsWith('listImportATagFailed:')) {
const preview = raw.slice('listImportATagFailed:'.length)
return t('listImportATagFailed', { preview })
}
return raw
}
export default function CreateSpellDialog({ export default function CreateSpellDialog({
open, open,
onOpenChange, onOpenChange,
@ -147,10 +250,18 @@ export default function CreateSpellDialog({
spellToClone?: NostrEvent | null spellToClone?: NostrEvent | null
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey, publish, checkLogin } = useNostr() const { pubkey, publish, checkLogin, relayList } = useNostr()
const [form, setForm] = useState<TSpellDraftParams>(DEFAULT_PARAMS) const [form, setForm] = useState<TSpellDraftParams>(DEFAULT_PARAMS)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const scrollBodyRef = useRef<HTMLDivElement>(null) const scrollBodyRef = useRef<HTMLDivElement>(null)
const formRef = useRef<TSpellDraftParams>(DEFAULT_PARAMS)
const [listImportNotices, setListImportNotices] = useState<string[]>([])
const [manualListRef, setManualListRef] = useState('')
const [manualListLoading, setManualListLoading] = useState(false)
useEffect(() => {
formRef.current = form
}, [form])
useEffect(() => { useEffect(() => {
if (!open) return if (!open) return
@ -160,8 +271,47 @@ export default function CreateSpellDialog({
} else { } else {
setForm({ ...DEFAULT_PARAMS }) setForm({ ...DEFAULT_PARAMS })
} }
setListImportNotices([])
setManualListRef('')
}, [open, spellToEdit, spellToClone]) }, [open, spellToEdit, spellToClone])
const applyListSource = useCallback(
(ev: NostrEvent) => {
const base = formRef.current
const { draft, notices, pendingATags } = applyListEventToSpellDraft(base, ev)
setForm(draft)
setListImportNotices(notices)
const urls = getRelaysForSpellCatalogSync(relayList ?? undefined)
if (pendingATags.length === 0) return
void resolveSpellListATags(pendingATags, urls).then(({ ids, notices: extra }) => {
if (ids.length) {
setForm((f) => ({ ...f, ids: dedupeAppendIds(f.ids, ids) }))
}
if (extra.length) setListImportNotices((n) => [...n, ...extra])
})
},
[relayList]
)
const handleLoadManualList = useCallback(async () => {
const q = manualListRef.trim()
if (!q) return
setManualListLoading(true)
try {
const ev = await client.fetchEvent(q)
if (!ev) {
setListImportNotices([t('listImportEventNotFound')])
return
}
applyListSource(ev)
} catch (e) {
logger.warn('[CreateSpellDialog] List import fetch failed', e)
setListImportNotices([t('listImportEventNotFound')])
} finally {
setManualListLoading(false)
}
}, [manualListRef, applyListSource, t])
const handleScrollBodyKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => { const handleScrollBodyKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
const el = scrollBodyRef.current const el = scrollBodyRef.current
if (!el) return if (!el) return
@ -186,7 +336,11 @@ export default function CreateSpellDialog({
} }
}, []) }, [])
const handleClear = () => setForm({ ...DEFAULT_PARAMS }) const handleClear = () => {
setForm({ ...DEFAULT_PARAMS })
setListImportNotices([])
setManualListRef('')
}
const handleCancel = () => { const handleCancel = () => {
handleClear() handleClear()
onOpenChange(false) onOpenChange(false)
@ -250,9 +404,7 @@ export default function CreateSpellDialog({
</DialogHeader> </DialogHeader>
<p className="mt-2 text-sm text-muted-foreground"> <p className="mt-2 text-sm text-muted-foreground">
{spellToClone {spellToClone
? t( ? t('Clone spell intro')
'This spell is preloaded from someone else’s definition. Adjust anything you like, then save to publish a new spell signed by you.'
)
: t( : 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.' '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.'
)} )}
@ -268,6 +420,44 @@ export default function CreateSpellDialog({
onKeyDown={handleScrollBodyKeyDown} onKeyDown={handleScrollBodyKeyDown}
> >
<div className="grid gap-4"> <div className="grid gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="spell-list-ref" className="text-sm">
{t('listImportManualLabel')}
</Label>
<p className="text-xs text-muted-foreground">{t('listImportFromEventHint')}</p>
<div className="flex flex-wrap gap-2">
<Input
id="spell-list-ref"
className="min-w-[12rem] flex-1 font-mono text-sm"
placeholder={t('listImportManualPlaceholder')}
value={manualListRef}
onChange={(e) => setManualListRef(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
void handleLoadManualList()
}
}}
/>
<Button
type="button"
variant="secondary"
className="shrink-0"
disabled={manualListLoading || !manualListRef.trim()}
onClick={() => void handleLoadManualList()}
>
{manualListLoading ? t('Loading...') : t('listImportLoadManual')}
</Button>
</div>
{listImportNotices.length > 0 ? (
<ul className="list-inside list-disc space-y-1 text-xs text-amber-800 dark:text-amber-200">
{listImportNotices.map((n, i) => (
<li key={`${n}-${i}`}>{formatListImportNotice(n, t)}</li>
))}
</ul>
) : null}
</div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label>{t('Command')}</Label> <Label>{t('Command')}</Label>
<select <select
@ -386,6 +576,11 @@ export default function CreateSpellDialog({
onChange={(topics) => setForm((f) => ({ ...f, topics }))} onChange={(topics) => setForm((f) => ({ ...f, topics }))}
/> />
<TagFiltersEditor
tagFilters={form.tagFilters}
onChange={(tagFilters) => setForm((f) => ({ ...f, tagFilters }))}
/>
{form.cmd === 'REQ' ? ( {form.cmd === 'REQ' ? (
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label>{t('Mode')}</Label> <Label>{t('Mode')}</Label>

8
src/services/indexed-db.service.ts

@ -50,7 +50,7 @@ export const StoreNames = {
} }
/** Schema version we expect. When adding stores or migrations, bump this. */ /** Schema version we expect. When adding stores or migrations, bump this. */
const DB_VERSION = 24 const DB_VERSION = 26
/** Max age for profile and payment info cache before we refetch (5 min). */ /** Max age for profile and payment info cache before we refetch (5 min). */
const PROFILE_AND_PAYMENT_CACHE_MAX_AGE_MS = 5 * 60 * 1000 const PROFILE_AND_PAYMENT_CACHE_MAX_AGE_MS = 5 * 60 * 1000
@ -136,6 +136,12 @@ class IndexedDbService {
request.onupgradeneeded = (event) => { request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result const db = (event.target as IDBOpenDBRequest).result
if (
event.oldVersion < 26 &&
db.objectStoreNames.contains('spellListSourceEvents')
) {
db.deleteObjectStore('spellListSourceEvents')
}
if (!db.objectStoreNames.contains(StoreNames.PROFILE_EVENTS)) { if (!db.objectStoreNames.contains(StoreNames.PROFILE_EVENTS)) {
db.createObjectStore(StoreNames.PROFILE_EVENTS, { keyPath: 'key' }) db.createObjectStore(StoreNames.PROFILE_EVENTS, { keyPath: 'key' })
} }

Loading…
Cancel
Save