Browse Source

bug-fixes

imwald
Silberengel 4 weeks ago
parent
commit
6b28bb5e52
  1. 106
      src/components/NoteList/index.tsx
  2. 3
      src/components/NoteStats/RepostButton.tsx
  3. 49
      src/lib/companion-publish.test.ts
  4. 214
      src/lib/companion-publish.ts
  5. 5
      src/lib/nostr-event-json.test.ts
  6. 30
      src/lib/nostr-event-json.ts
  7. 36
      src/lib/profile-author-warmup-spec.test.ts
  8. 59
      src/lib/profile-author-warmup-spec.ts
  9. 2
      src/pages/secondary/ProfileEditorPage/index.tsx
  10. 8
      src/providers/NostrProvider/index.tsx
  11. 7
      src/types/index.d.ts

106
src/components/NoteList/index.tsx

@ -78,6 +78,10 @@ import type { TPrimaryPageName } from '@/PageManager'
import { NoteFeedProfileContext, type NoteFeedProfileContextValue } from '@/providers/NoteFeedProfileContext' import { NoteFeedProfileContext, type NoteFeedProfileContextValue } from '@/providers/NoteFeedProfileContext'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { buildFeedFullSearchRelayUrls } from '@/lib/feed-full-search-relays' import { buildFeedFullSearchRelayUrls } from '@/lib/feed-full-search-relays'
import {
getProfileAuthorWarmupRelayUrls,
getProfileAuthorWarmupSpec
} from '@/lib/profile-author-warmup-spec'
import type { TProfile } from '@/types' import type { TProfile } from '@/types'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@ -592,36 +596,6 @@ function tightestSinceFromSpellFilters(shardFilters: Filter[]): number | undefin
return sinceCandidates.length > 0 ? Math.max(...sinceCandidates) : undefined return sinceCandidates.length > 0 ? Math.max(...sinceCandidates) : undefined
} }
/**
* Profile Posts / Media feeds shard by relay but share one author + kinds REQ. Session + IDB author scans are keyed
* only on that author/kinds pair. Timeline rows may live under per-shard persist keys; profile async warmup merges
* {@link ClientService.getTimelineDiskSnapshotEvents} with the author archive scan so both layers paint together.
*/
function getProfileSingleAuthorWarmupSpec(
mapped: Array<{ urls: string[]; filter: TSubRequestFilter }>
): { author: string; kinds: number[] } | null {
if (mapped.length === 0) return null
let normAuthor: string | null = null
const kindUnion = new Set<number>()
for (const { filter: f } of mapped) {
const authors = Array.isArray(f.authors) ? f.authors : undefined
if (!authors || authors.length !== 1) return null
let pk: string
try {
pk = normalizeHexPubkey(authors[0])
} catch {
return null
}
if (normAuthor === null) normAuthor = pk
else if (normAuthor !== pk) return null
const ks = Array.isArray(f.kinds) ? f.kinds : undefined
if (!ks || ks.length === 0) return null
for (const k of ks) kindUnion.add(k)
}
if (normAuthor === null) return null
return { author: normAuthor, kinds: Array.from(kindUnion).sort((a, b) => a - b) }
}
/** Union of `filter.kinds` across mapped REQ shards; empty if any shard omits kinds (caller should not use fallback). */ /** Union of `filter.kinds` across mapped REQ shards; empty if any shard omits kinds (caller should not use fallback). */
function filterEvsToMappedTimelineReqKinds( function filterEvsToMappedTimelineReqKinds(
evs: Event[], evs: Event[],
@ -2397,9 +2371,11 @@ const NoteList = forwardRef(
} }
})() })()
} else { } else {
const profileAuthorWarmSpec = getProfileSingleAuthorWarmupSpec( const profileMapped = mappedSubRequests as Array<{
mappedSubRequests as Array<{ urls: string[]; filter: TSubRequestFilter }> urls: string[]
) filter: TSubRequestFilter
}>
const profileAuthorWarmSpec = getProfileAuthorWarmupSpec(profileMapped)
if ( if (
hostPrimaryPageName === 'profile' && hostPrimaryPageName === 'profile' &&
profileAuthorWarmSpec && profileAuthorWarmSpec &&
@ -2477,6 +2453,44 @@ const NoteList = forwardRef(
setFeedEmptyToastGateTick((n) => n + 1) setFeedEmptyToastGateTick((n) => n + 1)
setFeedTimelineEmptyUiReady(true) setFeedTimelineEmptyUiReady(true)
} }
const relayUrls = getProfileAuthorWarmupRelayUrls(profileMapped)
if (relayUrls.length > 0) {
const fetched = await client.fetchEvents(
relayUrls,
{
authors: [profileAuthorWarmSpec.author],
kinds: profileAuthorWarmSpec.kinds,
limit: 200
},
{
cache: true,
eoseTimeout: 4500,
globalTimeout: 18_000,
replaceableRace: true
}
)
if (!effectActive || timelineEffectStale()) return
if (fetched.length === 0) return
const narrowedFetch = narrowLiveBatch(fetched)
if (narrowedFetch.length === 0) return
setEvents((prev) => {
const merged = collapseDuplicateNip18RepostTimelineRows(
mergeEventBatchesById(prev, narrowedFetch, eventCapEarly, areAlgoRelays)
)
if (merged.length > 0) {
timelineMergeBootstrapRef.current = merged.slice()
}
lastEventsForTimelinePrefetchRef.current = merged
return merged
})
feedRelayReturnedAnyEventRef.current = true
if (!feedPaintLiveRelayDoneRef.current) {
setLoading(false)
setFeedEmptyToastGateTick((n) => n + 1)
setFeedTimelineEmptyUiReady(true)
}
}
} catch { } catch {
/* profile local archive is best-effort */ /* profile local archive is best-effort */
} }
@ -3614,7 +3628,26 @@ const NoteList = forwardRef(
publicReadFallbackAttemptedRef.current = true publicReadFallbackAttemptedRef.current = true
const filter: Filter = { ...(mapped[0]!.filter as Filter) } const profileWarm =
hostPrimaryPageNameRef.current === 'profile'
? getProfileAuthorWarmupSpec(
mapped as Array<{ urls: string[]; filter: TSubRequestFilter }>
)
: null
const profileRelayUrls =
profileWarm != null
? getProfileAuthorWarmupRelayUrls(
mapped as Array<{ urls: string[]; filter: TSubRequestFilter }>
)
: []
const filter: Filter = profileWarm
? {
authors: [profileWarm.author],
kinds: profileWarm.kinds,
limit: LIMIT
}
: { ...(mapped[0]!.filter as Filter) }
if (!filter.kinds?.length) { if (!filter.kinds?.length) {
filter.kinds = effectiveShowKinds.length > 0 ? [...effectiveShowKinds] : [kinds.ShortTextNote] filter.kinds = effectiveShowKinds.length > 0 ? [...effectiveShowKinds] : [kinds.ShortTextNote]
} }
@ -3626,9 +3659,12 @@ const NoteList = forwardRef(
? ALGO_LIMIT ? ALGO_LIMIT
: LIMIT : LIMIT
const fallbackRelays =
profileRelayUrls.length > 0 ? profileRelayUrls : FAST_READ_RELAY_URLS
void (async () => { void (async () => {
try { try {
const raw = await client.fetchEvents(FAST_READ_RELAY_URLS, filter, { const raw = await client.fetchEvents(fallbackRelays, filter, {
cache: true, cache: true,
globalTimeout: 22_000, globalTimeout: 22_000,
eoseTimeout: 3500, eoseTimeout: 3500,

3
src/components/NoteStats/RepostButton.tsx

@ -72,8 +72,7 @@ export function RepostButtonWithStats({ event, hideCount = false, noteStats }: R
const repost = createRepostDraftEvent(event) const repost = createRepostDraftEvent(event)
const evt = await publish(repost, { const evt = await publish(repost, {
addClientTag: storage.getAddClientTag(), addClientTag: storage.getAddClientTag()
companionSourceEvent: event
}) })
// Show publishing feedback // Show publishing feedback

49
src/lib/companion-publish.test.ts

@ -1,49 +0,0 @@
import { describe, expect, it } from 'vitest'
import { kinds, nip19 } from 'nostr-tools'
import {
COMPANION_PUBLISH_CAP,
collectCompanionRefsInPublishOrder
} from './companion-publish'
const HEX_A = 'a'.repeat(64)
const HEX_B = 'b'.repeat(64)
const HEX_C = 'c'.repeat(64)
const HEX_D = 'd'.repeat(64)
const HEX_PUB = 'e'.repeat(64)
describe('collectCompanionRefsInPublishOrder', () => {
it('orders embedded before q before a before e', () => {
const note1 = nip19.noteEncode(HEX_B)
const ev = {
id: HEX_A,
kind: kinds.ShortTextNote,
content: `see nostr:${note1}`,
tags: [
['q', HEX_B],
['a', `30023:${HEX_PUB}:doc`, HEX_C],
['e', HEX_D]
],
pubkey: HEX_PUB,
created_at: 1,
sig: 's'.repeat(128)
}
const refs = collectCompanionRefsInPublishOrder(ev as never)
const tiers = refs.map((r) => r.tier)
const firstEmbedded = tiers.indexOf('embedded')
const firstQ = tiers.indexOf('q')
const firstA = tiers.indexOf('a')
const firstE = tiers.indexOf('e')
expect(firstEmbedded).toBeGreaterThanOrEqual(0)
expect(firstQ).toBeGreaterThan(firstEmbedded)
expect(firstA).toBeGreaterThan(firstQ)
expect(firstE).toBeGreaterThan(firstA)
})
})
describe('COMPANION_PUBLISH_CAP', () => {
it('is 5', () => {
expect(COMPANION_PUBLISH_CAP).toBe(5)
})
})

214
src/lib/companion-publish.ts

@ -1,214 +0,0 @@
import { EMBEDDED_EVENT_REGEX } from '@/lib/content-patterns'
import { relayHintWssUrlsFromEvent } from '@/lib/event'
import { findTrailingStringifiedNostrEvent } from '@/lib/nostr-event-json'
import client from '@/services/client.service'
import type { TPublishEventExtras } from '@/types'
import { nip19, type Event } from 'nostr-tools'
export const COMPANION_PUBLISH_CAP = 5
type CompanionRef =
| { tier: 'embedded'; hexId: string }
| { tier: 'embedded'; nip19: string }
| { tier: 'embedded'; inline: Event }
| { tier: 'q'; hexId: string }
| { tier: 'q'; coordinate: string }
| { tier: 'a'; hexId: string }
| { tier: 'a'; coordinate: string }
| { tier: 'e'; hexId: string }
function normalizeHex(id: string | undefined): string | undefined {
if (!id) return undefined
const t = id.trim().toLowerCase()
return /^[0-9a-f]{64}$/.test(t) ? t : undefined
}
function parseQOrATagValue(raw: string | undefined): { hexId?: string; coordinate?: string } | undefined {
if (raw == null) return undefined
let s0 = raw.trim()
if (s0.toLowerCase().startsWith('nostr:')) s0 = s0.slice(6).trim()
if (!s0) return undefined
const hex = normalizeHex(s0)
if (hex) return { hexId: hex }
const coordMatch = /^(\d+):([0-9a-f]{64}):(.*)$/i.exec(s0)
if (coordMatch) {
return {
coordinate: `${Number(coordMatch[1])}:${coordMatch[2].toLowerCase()}:${coordMatch[3]}`
}
}
if (/^n(?:ote|event|addr)1/i.test(s0)) {
try {
const { type, data } = nip19.decode(s0)
if (type === 'note') return { hexId: normalizeHex(typeof data === 'string' ? data : (data as { id?: string }).id) }
if (type === 'nevent') return { hexId: normalizeHex((data as { id: string }).id) }
if (type === 'naddr') {
const d = data as { kind: number; pubkey: string; identifier: string }
return {
coordinate: `${d.kind}:${d.pubkey.toLowerCase()}:${d.identifier ?? ''}`
}
}
} catch {
/* ignore */
}
}
return undefined
}
function collectEmbeddedRefsFromContent(ev: Event, out: CompanionRef[]): void {
for (const full of ev.content.match(EMBEDDED_EVENT_REGEX) ?? []) {
const colon = full.indexOf(':')
if (colon < 0) continue
const bech32 = full.slice(colon + 1)
try {
const { type, data } = nip19.decode(bech32)
if (type === 'note') {
const hex = normalizeHex(typeof data === 'string' ? data : (data as { id?: string }).id)
if (hex) out.push({ tier: 'embedded', hexId: hex })
} else if (type === 'nevent') {
const hex = normalizeHex((data as { id: string }).id)
if (hex) out.push({ tier: 'embedded', hexId: hex })
} else if (type === 'naddr') {
out.push({ tier: 'embedded', nip19: bech32 })
}
} catch {
/* ignore */
}
}
const trailing = findTrailingStringifiedNostrEvent(ev.content)
if (trailing) {
out.push({ tier: 'embedded', inline: trailing.event })
collectEmbeddedRefsFromContent(trailing.event, out)
}
}
/** Ordered refs: embedded (content + trailing JSON), then all `q`, then `a`, then `e`. */
export function collectCompanionRefsInPublishOrder(event: Event): CompanionRef[] {
const embedded: CompanionRef[] = []
const qRefs: CompanionRef[] = []
const aRefs: CompanionRef[] = []
const eRefs: CompanionRef[] = []
collectEmbeddedRefsFromContent(event, embedded)
for (const tag of event.tags) {
const name = tag[0]
if (name === 'q' || name === 'Q') {
const parsed = parseQOrATagValue(tag[1])
if (parsed?.hexId) qRefs.push({ tier: 'q', hexId: parsed.hexId })
else if (parsed?.coordinate) qRefs.push({ tier: 'q', coordinate: parsed.coordinate })
continue
}
if (name === 'a' || name === 'A') {
const snap = normalizeHex(tag[3])
if (snap) {
aRefs.push({ tier: 'a', hexId: snap })
continue
}
const parsed = parseQOrATagValue(tag[1])
if (parsed?.hexId) aRefs.push({ tier: 'a', hexId: parsed.hexId })
else if (parsed?.coordinate) aRefs.push({ tier: 'a', coordinate: parsed.coordinate })
continue
}
if (name === 'e' || name === 'E') {
const hex = normalizeHex(tag[1])
if (hex) eRefs.push({ tier: 'e', hexId: hex })
}
}
return [...embedded, ...qRefs, ...aRefs, ...eRefs]
}
/**
* Resolve referenced events for companion republish (boost target, quotes, replies with embeds).
* Order preserved: embedded q a e; capped at {@link COMPANION_PUBLISH_CAP}.
*/
export async function resolveCompanionEventsForPublish(
source: Event,
opts?: { excludeIds?: string[] }
): Promise<Event[]> {
const exclude = new Set(
[source.id, ...(opts?.excludeIds ?? [])].map((id) => id.trim().toLowerCase()).filter(Boolean)
)
const relayHints = relayHintWssUrlsFromEvent(source)
const fetchOpts = relayHints.length > 0 ? { relayHints } : undefined
const refs = collectCompanionRefsInPublishOrder(source)
const resolved: Event[] = []
const seen = new Set<string>()
const tryAdd = (ev: Event | undefined) => {
if (!ev) return false
const k = ev.id.toLowerCase()
if (exclude.has(k) || seen.has(k)) return false
seen.add(k)
resolved.push(ev)
return resolved.length >= COMPANION_PUBLISH_CAP
}
const resolveRef = async (ref: CompanionRef): Promise<Event | undefined> => {
if ('inline' in ref) return ref.inline
if ('nip19' in ref) {
try {
return await client.fetchEvent(ref.nip19, fetchOpts)
} catch {
return undefined
}
}
if ('coordinate' in ref) {
try {
return await client.fetchEvent(ref.coordinate, fetchOpts)
} catch {
return undefined
}
}
if ('hexId' in ref) {
try {
return await client.fetchEvent(ref.hexId, fetchOpts)
} catch {
return undefined
}
}
return undefined
}
for (const ref of refs) {
if (resolved.length >= COMPANION_PUBLISH_CAP) break
if ('inline' in ref) {
if (tryAdd(ref.inline)) break
continue
}
if ('hexId' in ref) {
const k = ref.hexId
if (exclude.has(k) || seen.has(k)) continue
}
const ev = await resolveRef(ref)
if (tryAdd(ev)) break
}
return resolved
}
/** Fire-and-forget friendly: publish companions to the same relays; never throws. */
export async function publishCompanionEventsBestEffort(
relayUrls: string[],
companions: readonly Event[],
extras: TPublishEventExtras
): Promise<void> {
if (!relayUrls.length || !companions.length) return
for (const companion of companions) {
try {
await client.publishEvent(relayUrls, companion, {
...extras,
publishBatchLabel: 'companion'
})
} catch {
/* best-effort */
}
}
}

5
src/lib/nostr-event-json.test.ts

@ -50,4 +50,9 @@ describe('nostr event JSON helpers', () => {
expect(findTrailingStringifiedNostrEvent(content)).toBeNull() expect(findTrailingStringifiedNostrEvent(content)).toBeNull()
expect(stripTrailingStringifiedNostrEvent(content)).toBe(content) expect(stripTrailingStringifiedNostrEvent(content)).toBe(content)
}) })
it('returns quickly when content has many braces but no trailing event', () => {
const content = 'x'.repeat(20_000) + '{'.repeat(10_000) + '}'
expect(findTrailingStringifiedNostrEvent(content)).toBeNull()
})
}) })

30
src/lib/nostr-event-json.ts

@ -44,35 +44,47 @@ function parseNostrEventJson(raw: string): Event | null {
} }
} }
/** Only scan the tail — trailing serialized events are never megabytes into the body. */
const MAX_TRAILING_SCAN_LEN = 256 * 1024
/** Profile metadata and prose can contain many `{`; cap work per call. */
const MAX_BRACE_ITERATIONS = 64
/** /**
* Some clients append a full serialized event after quote/repost text. Treat a trailing event JSON * Some clients append a full serialized event after quote/repost text. Treat a trailing event JSON
* object as structured data instead of showing it as prose. * object as structured data instead of showing it as prose.
*/ */
export function findTrailingStringifiedNostrEvent(content: string): StringifiedNostrEventMatch | null { export function findTrailingStringifiedNostrEvent(content: string): StringifiedNostrEventMatch | null {
const trimmed = content.trimEnd() const trimmed = content.trimEnd()
if (!trimmed) return null if (!trimmed || !trimmed.endsWith('}')) return null
const windowStart = Math.max(0, trimmed.length - MAX_TRAILING_SCAN_LEN)
const window = trimmed.slice(windowStart)
const windowOffset = windowStart
const whole = parseNostrEventJson(trimmed) const whole = parseNostrEventJson(window)
if (whole) { if (whole) {
return { return {
event: whole, event: whole,
textBefore: '', textBefore: trimmed.slice(0, windowOffset).trimEnd(),
jsonText: trimmed jsonText: window
} }
} }
let start = trimmed.lastIndexOf('{') let start = window.lastIndexOf('{')
while (start >= 0) { let iterations = 0
const jsonText = trimmed.slice(start) while (start >= 0 && iterations < MAX_BRACE_ITERATIONS) {
iterations += 1
const jsonText = window.slice(start)
const event = parseNostrEventJson(jsonText) const event = parseNostrEventJson(jsonText)
if (event) { if (event) {
const absStart = windowOffset + start
return { return {
event, event,
textBefore: trimmed.slice(0, start).trimEnd(), textBefore: trimmed.slice(0, absStart).trimEnd(),
jsonText jsonText
} }
} }
start = trimmed.lastIndexOf('{', start - 1) start = window.lastIndexOf('{', start - 1)
} }
return null return null

36
src/lib/profile-author-warmup-spec.test.ts

@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest'
import { ExtendedKind } from '@/constants'
import { getProfileAuthorWarmupSpec } from './profile-author-warmup-spec'
describe('getProfileAuthorWarmupSpec', () => {
const authorHex = 'a'.repeat(64)
it('returns spec when calendar #p shards omit authors', () => {
const spec = getProfileAuthorWarmupSpec([
{
urls: ['wss://relay.example'],
filter: { authors: [authorHex], kinds: [1], limit: 200 }
},
{
urls: ['wss://relay.example'],
filter: {
kinds: [ExtendedKind.CALENDAR_EVENT_DATE],
'#p': [authorHex],
limit: 100
}
}
])
expect(spec).toEqual({ author: authorHex, kinds: [1] })
})
it('returns null when no author shards', () => {
expect(
getProfileAuthorWarmupSpec([
{
urls: ['wss://relay.example'],
filter: { kinds: [ExtendedKind.CALENDAR_EVENT_DATE], '#p': [authorHex], limit: 100 }
}
])
).toBeNull()
})
})

59
src/lib/profile-author-warmup-spec.ts

@ -0,0 +1,59 @@
import type { TSubRequestFilter } from '@/types'
import { normalizeHexPubkey } from '@/lib/pubkey'
import type { Filter } from 'nostr-tools'
/**
* Profile feeds may include calendar invite shards (`#p`) without `authors`. Local session/IDB
* warmup and relay fallback only need the single-author + kinds REQ shards.
*/
export function getProfileAuthorWarmupSpec(
mapped: Array<{ urls: string[]; filter: TSubRequestFilter }>
): { author: string; kinds: number[] } | null {
const authorShards = mapped.filter((m) => {
const authors = (m.filter as Filter).authors
return Array.isArray(authors) && authors.length === 1
})
if (authorShards.length === 0) return null
let normAuthor: string | null = null
const kindUnion = new Set<number>()
for (const { filter: f } of authorShards) {
const authors = (f as Filter).authors!
let pk: string
try {
pk = normalizeHexPubkey(authors[0]!)
} catch {
return null
}
if (normAuthor === null) normAuthor = pk
else if (normAuthor !== pk) return null
const ks = (f as Filter).kinds
if (!Array.isArray(ks) || ks.length === 0) return null
for (const k of ks) kindUnion.add(k)
}
if (normAuthor === null || kindUnion.size === 0) return null
return { author: normAuthor, kinds: Array.from(kindUnion).sort((a, b) => a - b) }
}
/** Relay URLs from author shards only (for profile one-shot fetch). */
export function getProfileAuthorWarmupRelayUrls(
mapped: Array<{ urls: string[]; filter: TSubRequestFilter }>
): string[] {
const authorShards = mapped.filter((m) => {
const authors = (m.filter as Filter).authors
return Array.isArray(authors) && authors.length === 1
})
const seen = new Set<string>()
const out: string[] = []
for (const shard of authorShards) {
for (const u of shard.urls) {
if (!u || seen.has(u)) continue
seen.add(u)
out.push(u)
}
}
return out
}

2
src/pages/secondary/ProfileEditorPage/index.tsx

@ -274,7 +274,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
return return
} }
const draft = createPaymentInfoDraftEvent(contentStr, tags) const draft = createPaymentInfoDraftEvent(contentStr, tags)
const published = await publish(draft, { skipCompanionPublish: true }) const published = await publish(draft)
await client.updatePaymentInfoCache(published) await client.updatePaymentInfoCache(published)
setPaymentInfoEvent(published) setPaymentInfoEvent(published)
setPaymentInfoEditOpen(false) setPaymentInfoEditOpen(false)

8
src/providers/NostrProvider/index.tsx

@ -1624,14 +1624,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
skipOutboxRetry: (options.specifiedRelayUrls?.length ?? 0) > 0 skipOutboxRetry: (options.specifiedRelayUrls?.length ?? 0) > 0
} }
const publishResult = await client.publishEvent(relays, event, publishExtras) const publishResult = await client.publishEvent(relays, event, publishExtras)
if (publishResult.successCount >= 1 && !options.skipCompanionPublish) {
const companionSource = options.companionSourceEvent ?? event
void import('@/lib/companion-publish').then(({ resolveCompanionEventsForPublish, publishCompanionEventsBestEffort }) =>
resolveCompanionEventsForPublish(companionSource, { excludeIds: [event.id] }).then((companions) =>
publishCompanionEventsBestEffort(relays, companions, publishExtras)
)
)
}
logger.debug('[Publish] publishEvent completed', { logger.debug('[Publish] publishEvent completed', {
success: publishResult.success, success: publishResult.success,
successCount: publishResult.successCount, successCount: publishResult.successCount,

7
src/types/index.d.ts vendored

@ -201,13 +201,6 @@ export type TPublishOptions = {
disableFallbacks?: boolean // If true, don't use fallback relays when publishing fails disableFallbacks?: boolean // If true, don't use fallback relays when publishing fails
/** Override global "Add client tag" preference for this publish (default: read from localStorage) */ /** Override global "Add client tag" preference for this publish (default: read from localStorage) */
addClientTag?: boolean addClientTag?: boolean
/**
* Resolve companion republishes from this event (default: the event being published).
* Use when the published event is a wrapper (e.g. boost) but embeds live on the target note.
*/
companionSourceEvent?: Event
/** Skip automatic companion republish of embedded / q / a / e references. */
skipCompanionPublish?: boolean
} }
/** Options for {@link ClientService.publishEvent} (second argument bundle in code: favorites + internal retry pass). */ /** Options for {@link ClientService.publishEvent} (second argument bundle in code: favorites + internal retry pass). */

Loading…
Cancel
Save