Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
1cca346519
  1. 105
      src/components/LatestFromFollowsSection/index.tsx
  2. 10
      src/constants.ts
  3. 33
      src/hooks/useFetchCalendarRsvps.tsx
  4. 29
      src/hooks/useProfileTimeline.tsx
  5. 8
      src/lib/draft-event.ts
  6. 20
      src/lib/favorites-feed-relays.ts
  7. 8
      src/providers/NostrProvider/index.tsx
  8. 8
      src/providers/UserTrustProvider.tsx
  9. 17
      src/services/client.service.ts
  10. 51
      src/services/indexed-db.service.ts

105
src/components/LatestFromFollowsSection/index.tsx

@ -60,17 +60,21 @@ const FEED_KINDS = [ @@ -60,17 +60,21 @@ const FEED_KINDS = [
const feedKindSet = new Set(FEED_KINDS)
const LOG = '[LatestFromFollows]'
function mergeBatchPosts(
prev: Map<string, NostrEvent[]>,
incoming: NostrEvent[],
batchAuthors: string[]
): Map<string, NostrEvent[]> {
const next = new Map(prev)
const authorSet = new Set(batchAuthors)
const filtered = incoming.filter((e) => authorSet.has(e.pubkey))
/** Follow list pubkeys are lowercased in `getPubkeysFromPTags`; relay `pubkey` may be mixed-case hex. */
const authorSet = new Set(batchAuthors.map((a) => a.toLowerCase()))
const filtered = incoming.filter((e) => authorSet.has(e.pubkey.toLowerCase()))
for (const pk of batchAuthors) {
const pkNorm = pk.toLowerCase()
const prevList = next.get(pk) ?? []
const newForPk = filtered.filter((e) => e.pubkey === pk)
const newForPk = filtered.filter((e) => e.pubkey.toLowerCase() === pkNorm)
const byId = new Map<string, NostrEvent>()
for (const e of prevList) byId.set(e.id, e)
for (const e of newForPk) {
@ -188,11 +192,13 @@ export default function LatestFromFollowsSection({ @@ -188,11 +192,13 @@ export default function LatestFromFollowsSection({
setGuestFollowPubkeys([])
;(async () => {
logger.info(`${LOG} guest: loading recommended follow list`)
const hex = recommendedCuratorHexPubkey()
if (!hex) {
if (!cancelled) {
setGuestFollowPubkeys([])
setGuestListReady(true)
logger.info(`${LOG} guest: no curator npub; follow list empty`)
}
return
}
@ -201,6 +207,7 @@ export default function LatestFromFollowsSection({ @@ -201,6 +207,7 @@ export default function LatestFromFollowsSection({
if (cancelled) return
const list = evt ? getPubkeysFromPTags(evt.tags).slice(0, MAX_FOLLOWS) : []
setGuestFollowPubkeys(list)
logger.info(`${LOG} guest: follow list loaded`, { count: list.length })
} catch (err) {
logger.warn('[LatestFromFollows] Failed to load recommended follow list', err)
if (!cancelled) setGuestFollowPubkeys([])
@ -217,9 +224,16 @@ export default function LatestFromFollowsSection({ @@ -217,9 +224,16 @@ export default function LatestFromFollowsSection({
// Load each follow's NIP-65 list (IndexedDB + network), then aggregate first outboxes + READ_ONLY relays.
useEffect(() => {
if (!isInitialized || loadingFollowList) {
logger.info(`${LOG} relays: waiting`, {
isInitialized,
loadingFollowList,
variant,
followsLabel
})
return
}
if (followPubkeys.length === 0) {
logger.info(`${LOG} relays: no follows; skipping aggregate`)
setAggregateRelayUrls([])
setAggregateRelaysReady(true)
return
@ -230,6 +244,11 @@ export default function LatestFromFollowsSection({ @@ -230,6 +244,11 @@ export default function LatestFromFollowsSection({
setAggregateRelayUrls([])
;(async () => {
logger.info(`${LOG} relays: fetch NIP-65 lists start`, {
authorCount: followPubkeys.length,
variant,
followsLabel
})
try {
// Dynamic import avoids a static cycle: client.service → replaceable-events → client.service
// (would break React context / HMR when this module loads early).
@ -248,28 +267,53 @@ export default function LatestFromFollowsSection({ @@ -248,28 +267,53 @@ export default function LatestFromFollowsSection({
favoriteRelays
)
setAggregateRelayUrls(urls)
logger.info(`${LOG} relays: aggregate URLs computed → setState`, {
nip65ListsLoaded: allLists.length,
aggregateUrlCount: urls.length,
relaySample: urls.slice(0, 6)
})
} catch (err) {
logger.warn('[LatestFromFollows] Failed to build follow outbox aggregate relays', err)
if (!cancelled) {
setAggregateRelayUrls(
buildFollowOutboxAggregateReadUrls([], blockedRelays, favoriteRelays)
)
const fallback = buildFollowOutboxAggregateReadUrls([], blockedRelays, favoriteRelays)
setAggregateRelayUrls(fallback)
logger.info(`${LOG} relays: using fallback aggregate URLs after error`, {
aggregateUrlCount: fallback.length
})
}
} finally {
if (!cancelled) setAggregateRelaysReady(true)
if (!cancelled) {
setAggregateRelaysReady(true)
logger.info(`${LOG} relays: aggregateRelaysReady → true`)
}
}
})()
return () => {
cancelled = true
}
}, [followPubkeys, favoriteRelays, blockedRelays, isInitialized, loadingFollowList])
}, [followPubkeys, favoriteRelays, blockedRelays, isInitialized, loadingFollowList, variant, followsLabel])
// Batch-fetch posts per slice of authors against the aggregate relay set.
useEffect(() => {
if (!isInitialized || loadingFollowList) return
if (followPubkeys.length === 0) return
if (!aggregateRelaysReady) return
if (!isInitialized || loadingFollowList) {
logger.info(`${LOG} posts: waiting`, {
isInitialized,
loadingFollowList,
aggregateRelaysReady,
followCount: followPubkeys.length,
variant
})
return
}
if (followPubkeys.length === 0) {
logger.info(`${LOG} posts: no follows; skipping batch fetch`)
return
}
if (!aggregateRelaysReady) {
logger.info(`${LOG} posts: waiting for aggregate relays`)
return
}
abortedRef.current = false
let cancelled = false
@ -280,6 +324,24 @@ export default function LatestFromFollowsSection({ @@ -280,6 +324,24 @@ export default function LatestFromFollowsSection({
let working = seed ? postsRecordToMap(seed.posts) : new Map<string, NostrEvent[]>()
setPostsByPubkey(new Map(working))
const summarizePosts = (m: Map<string, NostrEvent[]>) => {
let authorsWithPosts = 0
let totalNotes = 0
for (const arr of m.values()) {
if (arr.length > 0) authorsWithPosts++
totalNotes += arr.length
}
return { authorsWithPosts, totalNotes, mapKeyCount: m.size }
}
logger.info(`${LOG} posts: batch run start`, {
followCount: followPubkeys.length,
relayUrlCount: aggregateRelayUrls.length,
hideUntrustedNotes,
usedCacheSeed: Boolean(seed),
...summarizePosts(working)
})
const persist = () => {
writeSearchFollowsFeedCache({
v: 1,
@ -289,10 +351,18 @@ export default function LatestFromFollowsSection({ @@ -289,10 +351,18 @@ export default function LatestFromFollowsSection({
})
}
const batchCount = Math.ceil(followPubkeys.length / AUTHORS_PER_BATCH)
for (let i = 0; i < followPubkeys.length; i += AUTHORS_PER_BATCH) {
if (cancelled || abortedRef.current) break
const batch = followPubkeys.slice(i, i + AUTHORS_PER_BATCH)
const batchIndex = Math.floor(i / AUTHORS_PER_BATCH) + 1
try {
logger.info(`${LOG} posts: REQ batch ${batchIndex}/${batchCount}`, {
authorBatchSize: batch.length,
kinds: FEED_KINDS.length,
limit: BATCH_EVENT_LIMIT
})
const t0 = performance.now()
const raw = await queryService.fetchEvents(
aggregateRelayUrls,
{
@ -302,11 +372,19 @@ export default function LatestFromFollowsSection({ @@ -302,11 +372,19 @@ export default function LatestFromFollowsSection({
},
{ eoseTimeout: 2800, globalTimeout: 9000 }
)
const ms = Math.round(performance.now() - t0)
if (cancelled || abortedRef.current) break
const filtered = raw.filter((e) => acceptEvent(e))
working = mergeBatchPosts(working, filtered, batch)
setPostsByPubkey(new Map(working))
persist()
logger.info(`${LOG} posts: batch ${batchIndex}/${batchCount} done + UI setPostsByPubkey`, {
ms,
rawFromRelays: raw.length,
afterAcceptFilter: filtered.length,
droppedByAccept: raw.length - filtered.length,
...summarizePosts(working)
})
} catch (err) {
logger.warn('[LatestFromFollows] Batch fetch failed', { err, batchSize: batch.length })
}
@ -314,6 +392,10 @@ export default function LatestFromFollowsSection({ @@ -314,6 +392,10 @@ export default function LatestFromFollowsSection({
if (!cancelled) {
persist()
setBatchBusy(false)
logger.info(`${LOG} posts: batch run finished`, {
cancelled: false,
...summarizePosts(working)
})
}
}
@ -322,6 +404,7 @@ export default function LatestFromFollowsSection({ @@ -322,6 +404,7 @@ export default function LatestFromFollowsSection({
cancelled = true
abortedRef.current = true
setBatchBusy(false)
logger.info(`${LOG} posts: batch effect cleanup (cancelled / deps changed)`)
}
}, [
followPubkeys,

10
src/constants.ts

@ -113,6 +113,16 @@ export const TIMELINE_SHARD_SUBSCRIBE_CONCURRENCY = 3 @@ -113,6 +113,16 @@ export const TIMELINE_SHARD_SUBSCRIBE_CONCURRENCY = 3
/** Max relays to publish each event to (outboxes first, then targets' inboxes, then extras). */
export const MAX_PUBLISH_RELAYS = 20
/**
* Kind 24 / 31925: {@link mergeRelayPriorityLayers} used the full {@link MAX_PUBLISH_RELAYS} budget on the authors
* outbox list first, so recipient **read** inboxes were often never reached. This higher cap plus an author slice
* (see client.service) reserves space for organizer/recipient relays.
*/
export const PUBLIC_MESSAGE_RSVP_PUBLISH_MAX_RELAYS = 28
/** When publishing kind 24 / 31925 to recipients, only the first N author outbox URLs fill tier‑1 before inboxes. */
export const PUBLIC_MESSAGE_RSVP_PUBLISH_AUTHOR_WRITE_CAP = 10
/** After a publish wave, failed NIP-65 write (outbox) relays are retried once after this delay. */
export const OUTBOX_PUBLISH_RETRY_DELAY_MS = 5000

33
src/hooks/useFetchCalendarRsvps.tsx

@ -15,6 +15,20 @@ import { FAST_READ_RELAY_URLS } from '@/constants' @@ -15,6 +15,20 @@ import { FAST_READ_RELAY_URLS } from '@/constants'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import { tagNameEquals } from '@/lib/tag'
/** NIP-65 inboxes only — calendar RSVPs are published to the author’s outboxes, so REQ must include those too. */
function userWriteRelaysForQuery(
relayList: { write?: string[]; httpWrite?: string[] } | null | undefined
): string[] {
if (!relayList) return []
const ws = (relayList.write ?? [])
.map((url) => normalizeAnyRelayUrl(url) || url)
.filter(Boolean) as string[]
const http = (relayList.httpWrite ?? [])
.map((url) => normalizeAnyRelayUrl(url) || url)
.filter(Boolean) as string[]
return [...http, ...ws]
}
function getRsvpStatus(rsvp: Event): 'accepted' | 'tentative' | 'declined' | undefined {
const status = rsvp.tags.find(tagNameEquals('status'))?.[1]
if (status === 'accepted' || status === 'tentative' || status === 'declined') return status
@ -23,9 +37,10 @@ function getRsvpStatus(rsvp: Event): 'accepted' | 'tentative' | 'declined' | und @@ -23,9 +37,10 @@ function getRsvpStatus(rsvp: Event): 'accepted' | 'tentative' | 'declined' | und
function mergeRsvp(prev: Event[], evt: Event): Event[] {
const next = prev.filter((e) => e.id !== evt.id)
const samePubkey = next.find((e) => e.pubkey === evt.pubkey)
const pk = evt.pubkey.toLowerCase()
const samePubkey = next.find((e) => e.pubkey.toLowerCase() === pk)
if (samePubkey && samePubkey.created_at >= evt.created_at) return next
const withoutSamePubkey = samePubkey ? next.filter((e) => e.pubkey !== evt.pubkey) : next
const withoutSamePubkey = samePubkey ? next.filter((e) => e.pubkey.toLowerCase() !== pk) : next
return [...withoutSamePubkey, evt].sort((a, b) => b.created_at - a.created_at)
}
@ -55,8 +70,10 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) { @@ -55,8 +70,10 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
getReplaceableCoordinateFromEvent(calendarEvent)
)
const userRead = userReadRelaysWithHttp(relayList)
const userWrite = userWriteRelaysForQuery(relayList)
void (async () => {
// Read order: IndexedDB first (offline + last session), then in-memory session, then relays.
let fromIdb: Event[] = []
try {
fromIdb = await indexedDb.getCalendarRsvpEventsByParentCoordinate(coordinate)
@ -67,11 +84,12 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) { @@ -67,11 +84,12 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
const fromSession = client.getSessionCalendarRsvpsForCalendarEvent(calendarEvent)
const mergedLocal = mergeRsvpList([...fromIdb, ...fromSession])
if (mergedLocal.length) setRsvps(mergedLocal)
setRsvps(mergedLocal)
const baseUrls = new Set<string>([
...FAST_READ_RELAY_URLS.map((url) => normalizeAnyRelayUrl(url) || url),
...userRead.map((url) => normalizeAnyRelayUrl(url) || url)
...userRead.map((url) => normalizeAnyRelayUrl(url) || url),
...userWrite.map((url) => normalizeAnyRelayUrl(url) || url)
].filter(Boolean) as string[])
const organizerPubkey = calendarEvent.pubkey
@ -120,7 +138,11 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) { @@ -120,7 +138,11 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
}
)
if (cancelled) return
setRsvps(mergeRsvpList([...fromIdb, ...fromSession, ...(events ?? [])]))
const fromRelay = events ?? []
await Promise.allSettled(
fromRelay.map((ev) => indexedDb.putCalendarRsvpEventRow(ev).catch(() => undefined))
)
setRsvps(mergeRsvpList([...fromIdb, ...fromSession, ...fromRelay]))
} finally {
if (!cancelled) setIsFetching(false)
}
@ -150,6 +172,7 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) { @@ -150,6 +172,7 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
const matchesA = aCoord !== '' && aCoord === coordinate
const matchesE = eTag && /^[0-9a-f]{64}$/.test(eTag) && eTag === calId
if (!matchesA && !matchesE) return
void indexedDb.putCalendarRsvpEventRow(evt).catch(() => undefined)
setRsvps((prev) => mergeRsvp(prev, evt))
}

29
src/hooks/useProfileTimeline.tsx

@ -2,12 +2,13 @@ import { useDeletedEvent } from '@/providers/DeletedEventProvider' @@ -2,12 +2,13 @@ import { useDeletedEvent } from '@/providers/DeletedEventProvider'
import client from '@/services/client.service'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Event } from 'nostr-tools'
import { CALENDAR_EVENT_KINDS, ExtendedKind, isSocialKindBlockedKind } from '@/constants'
import { CALENDAR_EVENT_KINDS, ExtendedKind, isDocumentRelayKind, isSocialKindBlockedKind } from '@/constants'
import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays'
import { hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey'
import { normalizeAnyRelayUrl, subtractNormalizedRelayUrls } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostrOptional } from '@/providers/nostr-context'
import indexedDb from '@/services/indexed-db.service'
type ProfileTimelineMemoryEntry = {
events: Event[]
@ -221,9 +222,30 @@ export function useProfileTimeline({ @@ -221,9 +222,30 @@ export function useProfileTimeline({
blockedRelays,
emptyAuthor,
socialKinds,
includeAuthorLocalRelays
includeAuthorLocalRelays,
kinds
)
const idbDocKinds = kinds.filter((k) => isDocumentRelayKind(k))
if (idbDocKinds.length > 0) {
try {
const pkNorm = normalizeHexPubkey(pubkey)
const fromIdb = await indexedDb.getCachedPublicationStoreEventsForProfileAuthor(
pkNorm,
idbDocKinds,
limit
)
if (!cancelled) {
for (const e of fromIdb) {
pool.set(e.id, e)
}
if (fromIdb.length) flushPool()
}
} catch {
/* IDB optional */
}
}
const startWave = async (subRequests: ReturnType<typeof buildSubRequests>) => {
if (cancelled || subRequests.length === 0) return
try {
@ -273,7 +295,8 @@ export function useProfileTimeline({ @@ -273,7 +295,8 @@ export function useProfileTimeline({
blockedRelays,
authorRl,
socialKinds,
includeAuthorLocalRelays
includeAuthorLocalRelays,
kinds
)
const deltaUrls = subtractNormalizedRelayUrls(fullFeedUrls, provisionalFeedUrls)
if (cancelled || deltaUrls.length === 0) return

8
src/lib/draft-event.ts

@ -20,6 +20,7 @@ import { Event, kinds, nip19 } from 'nostr-tools' @@ -20,6 +20,7 @@ import { Event, kinds, nip19 } from 'nostr-tools'
import {
getReplaceableCoordinate,
getReplaceableCoordinateFromEvent,
normalizeReplaceableCoordinateString,
getRootETag,
isProtectedEvent,
isReplaceableEvent,
@ -680,11 +681,14 @@ export function createCalendarRsvpDraftEvent( @@ -680,11 +681,14 @@ export function createCalendarRsvpDraftEvent(
status: 'accepted' | 'tentative' | 'declined',
options: { content?: string; fb?: 'free' | 'busy' } = {}
): TDraftEvent {
const coordinate = getReplaceableCoordinateFromEvent(calendarEvent)
const coordinate = normalizeReplaceableCoordinateString(getReplaceableCoordinateFromEvent(calendarEvent))
const hint = client.getEventHint(calendarEvent.id)
const calendarHexId = /^[0-9a-f]{64}$/i.test(calendarEvent.id)
? calendarEvent.id.toLowerCase()
: calendarEvent.id
const tags: string[][] = [
['a', coordinate, hint ?? ''],
['e', calendarEvent.id, hint ?? ''],
['e', calendarHexId, hint ?? ''],
['d', randomString(12)],
['status', status],
['p', calendarEvent.pubkey]

20
src/lib/favorites-feed-relays.ts

@ -1,7 +1,9 @@ @@ -1,7 +1,9 @@
import {
DEFAULT_FAVORITE_RELAYS,
DOCUMENT_RELAY_URLS,
FAST_READ_RELAY_URLS,
READ_ONLY_RELAY_URLS,
isDocumentRelayKind,
relayFilterIncludesSocialKindBlockedKind
} from '@/constants'
import type { TFeedSubRequest } from '@/types'
@ -163,6 +165,9 @@ export function getRelayUrlsWithFavoritesFastReadAndInbox( @@ -163,6 +165,9 @@ export function getRelayUrlsWithFavoritesFastReadAndInbox(
/** Profile REQ cap: too small waits on a few bad relays; larger spreads load across fast-read / favorites. */
const PROFILE_PAGE_FEED_MAX_RELAYS = 14
/** Long-form / publication profile tab: slightly larger cap + {@link DOCUMENT_RELAY_URLS} merge. */
const PROFILE_PAGE_DOCUMENT_FEED_MAX_RELAYS = 24
export const PROFILE_PAGE_PINS_RESOLVE_LIMIT = 10
export function buildProfilePageReadRelayUrls(
@ -170,22 +175,31 @@ export function buildProfilePageReadRelayUrls( @@ -170,22 +175,31 @@ export function buildProfilePageReadRelayUrls(
blockedRelays: string[],
authorRelayList: { read: string[]; write: string[]; httpRead?: string[]; httpWrite?: string[] },
kindsIncludeSocialBlockedKind: boolean,
includeAuthorLocalRelays = false
includeAuthorLocalRelays = false,
/** When the timeline includes document kinds (30023, 30040, …), add document index relays and raise the cap. */
profileKindsHint?: readonly number[]
): string[] {
const wantsDocumentLayer = profileKindsHint?.some((k) => isDocumentRelayKind(k)) ?? false
const maxRelays = wantsDocumentLayer ? PROFILE_PAGE_DOCUMENT_FEED_MAX_RELAYS : PROFILE_PAGE_FEED_MAX_RELAYS
const list = includeAuthorLocalRelays
? authorRelayList
: stripMailboxLocalUrlsForRemoteViewers(authorRelayList)
return getRelayUrlsWithFavoritesFastReadAndInbox(
let urls = getRelayUrlsWithFavoritesFastReadAndInbox(
favoriteRelays,
blockedRelays,
[...(list.httpRead ?? []), ...(list.read ?? [])],
{
userWriteRelays: [...(list.httpWrite ?? []), ...(list.write ?? [])],
authorWriteRelays: [],
maxRelays: PROFILE_PAGE_FEED_MAX_RELAYS,
maxRelays,
applySocialKindBlockedFilter: kindsIncludeSocialBlockedKind
}
)
if (wantsDocumentLayer) {
const docLayer = DOCUMENT_RELAY_URLS.map((u) => normalizeUrl(u) || u).filter(Boolean) as string[]
urls = mergeRelayUrlLayers([urls, docLayer], blockedRelays).slice(0, maxRelays + 6)
}
return urls
}
/**

8
src/providers/NostrProvider/index.tsx

@ -1314,6 +1314,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1314,6 +1314,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
// If at least one relay accepted, cache and emit immediately so UI shows the event without waiting
if (publishResult.successCount >= 1) {
client.addEventToCache(event)
// Calendar RSVPs: durable store before `newEvent` so hooks that re-read IDB see this row first.
if (event.kind === ExtendedKind.CALENDAR_EVENT_RSVP) {
try {
await indexedDb.putCalendarRsvpEventRow(event)
} catch (err) {
logger.warn('[Publish] Calendar RSVP IndexedDB persist failed', { err })
}
}
client.emitNewEvent(event)
// Replaceable list events (pins, cache relays, …) must hit IndexedDB + DataLoader, not only RAM
void replaceableEventService.updateReplaceableEventCache(event).catch(() => {})

8
src/providers/UserTrustProvider.tsx

@ -34,7 +34,7 @@ export function UserTrustProvider({ children }: { children: ReactNode }) { @@ -34,7 +34,7 @@ export function UserTrustProvider({ children }: { children: ReactNode }) {
const initWoT = async () => {
const followListEvent = await replaceableEventService.fetchReplaceableEvent(currentPubkey, kinds.Contacts)
const followings = followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []
followings.forEach((pubkey) => wotSet.add(pubkey))
followings.forEach((pubkey) => wotSet.add(pubkey.toLowerCase()))
const batchSize = 20
for (let i = 0; i < followings.length; i += batchSize) {
@ -44,7 +44,7 @@ export function UserTrustProvider({ children }: { children: ReactNode }) { @@ -44,7 +44,7 @@ export function UserTrustProvider({ children }: { children: ReactNode }) {
const followListEvent = await replaceableEventService.fetchReplaceableEvent(pubkey, kinds.Contacts)
const _followings = followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []
_followings.forEach((following) => {
wotSet.add(following)
wotSet.add(following.toLowerCase())
})
})
)
@ -56,8 +56,8 @@ export function UserTrustProvider({ children }: { children: ReactNode }) { @@ -56,8 +56,8 @@ export function UserTrustProvider({ children }: { children: ReactNode }) {
const isUserTrusted = useCallback(
(pubkey: string) => {
if (!currentPubkey || pubkey === currentPubkey) return true
return wotSet.has(pubkey)
if (!currentPubkey || pubkey.toLowerCase() === currentPubkey.toLowerCase()) return true
return wotSet.has(pubkey.toLowerCase())
},
[currentPubkey]
)

17
src/services/client.service.ts

@ -14,6 +14,8 @@ import { @@ -14,6 +14,8 @@ import {
SOCIAL_KIND_BLOCKED_RELAY_URLS,
MAX_CONCURRENT_RELAY_CONNECTIONS,
MAX_PUBLISH_RELAYS,
PUBLIC_MESSAGE_RSVP_PUBLISH_AUTHOR_WRITE_CAP,
PUBLIC_MESSAGE_RSVP_PUBLISH_MAX_RELAYS,
PUBLISH_PRIORITIZE_RELAY_ORDER_TIMEOUT_MS,
PUBLISH_RELAY_LIST_RESOLUTION_TIMEOUT_MS,
RELAY_NIP42_PUBLISH_ACK_TIMEOUT_MS,
@ -895,10 +897,21 @@ class ClientService extends EventTarget { @@ -895,10 +897,21 @@ class ClientService extends EventTarget {
...(rl?.read ?? []).map((url) => normalizeUrl(url)).filter((u): u is string => !!u && !isLocalNetworkUrl(u))
])
recipientRead = dedupeNormalizeRelayUrlsOrdered(recipientRead)
const authorWriteOrdered = relayUrlsLocalsFirst(authorWrite)
/** Without this, tier‑1 author outboxes can consume all of {@link MAX_PUBLISH_RELAYS} and organizer inboxes never receive RSVPs. */
const recipientReadDeduped = recipientRead
const authorTier1Cap =
recipientReadDeduped.length > 0
? Math.min(PUBLIC_MESSAGE_RSVP_PUBLISH_AUTHOR_WRITE_CAP, authorWriteOrdered.length)
: authorWriteOrdered.length
const authorPrimary = authorWriteOrdered.slice(0, authorTier1Cap)
const authorOverflow = authorWriteOrdered.slice(authorTier1Cap)
const publishCap =
recipientReadDeduped.length > 0 ? PUBLIC_MESSAGE_RSVP_PUBLISH_MAX_RELAYS : MAX_PUBLISH_RELAYS
let pubRelays = mergeRelayPriorityLayers(
[relayUrlsLocalsFirst(authorWrite), dedupeNormalizeRelayUrlsOrdered(recipientRead)],
[authorPrimary, recipientReadDeduped, authorOverflow],
blockedRelayUrls,
MAX_PUBLISH_RELAYS,
publishCap,
{ applySocialKindBlockedFilter: false }
)
pubRelays = this.filterPublishingRelays(pubRelays, event)

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

@ -1173,6 +1173,7 @@ class IndexedDbService { @@ -1173,6 +1173,7 @@ class IndexedDbService {
case ExtendedKind.PUBLICATION:
case ExtendedKind.PUBLICATION_CONTENT:
case ExtendedKind.WIKI_ARTICLE:
case ExtendedKind.WIKI_ARTICLE_MARKDOWN:
case kinds.LongFormArticle:
return StoreNames.PUBLICATION_EVENTS
case ExtendedKind.BADGE_DEFINITION:
@ -1350,6 +1351,56 @@ class IndexedDbService { @@ -1350,6 +1351,56 @@ class IndexedDbService {
})
}
/**
* Cached long-form / wiki / publication rows for a profile author (same store as {@link putReplaceableEvent} for
* those kinds). Used to hydrate the profile Articles and publications tab before relay SUB results arrive.
*/
async getCachedPublicationStoreEventsForProfileAuthor(
pubkeyHex: string,
allowedKinds: number[],
limit: number
): Promise<Event[]> {
const pk = pubkeyHex.trim().toLowerCase()
if (!/^[0-9a-f]{64}$/.test(pk) || allowedKinds.length === 0 || limit <= 0) {
return []
}
await this.initPromise
if (!this.db?.objectStoreNames.contains(StoreNames.PUBLICATION_EVENTS)) {
return []
}
const kindSet = new Set(allowedKinds)
const max = Math.min(Math.max(limit, 1), 500)
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(StoreNames.PUBLICATION_EVENTS, 'readonly')
const store = transaction.objectStore(StoreNames.PUBLICATION_EVENTS)
const request = store.openCursor()
const results: Event[] = []
request.onsuccess = () => {
const cursor = (request as IDBRequest<IDBCursorWithValue>).result
if (!cursor || results.length >= max) {
transaction.commit()
resolve(results)
return
}
const item = cursor.value as TValue<Event> | undefined
if (item?.value) {
const event = item.value as Event
if (kindSet.has(event.kind) && event.pubkey?.toLowerCase() === pk) {
results.push(event)
}
}
cursor.continue()
}
request.onerror = (event) => {
transaction.commit()
reject(event)
}
})
}
/**
* Iterate PUBLICATION_EVENTS and return events whose kind is in allowedKinds and content or tags
* match the search query (case-insensitive). Used by nevent/naddr picker to show cached events first.

Loading…
Cancel
Save