Browse Source

bug-fixes

imwald
Silberengel 3 weeks ago
parent
commit
aa3cf0993d
  1. 4
      src/components/Note/Superchat.tsx
  2. 4
      src/components/Note/Zap.tsx
  3. 53
      src/components/NoteList/index.tsx
  4. 2
      src/components/TurnIntoSuperchatButton/index.tsx
  5. 24
      src/hooks/usePaymentAttestationStatus.test.ts
  6. 100
      src/hooks/usePaymentAttestationStatus.tsx
  7. 1
      src/i18n/locales/en.ts
  8. 4
      src/lib/payment-attestation-cache.ts
  9. 15
      src/services/client.service.ts

4
src/components/Note/Superchat.tsx

@ -151,10 +151,10 @@ export default function Superchat({ @@ -151,10 +151,10 @@ export default function Superchat({
{comment ? (
<SuperchatCommentMarkdown event={event} comment={comment} className="mt-2" />
) : null}
{!isProfileWall ? (
{isNotification ? (
<TurnIntoSuperchatButton
event={event}
prominent={isNotification}
prominent
attestationRecipientPubkey={recipientPubkey}
className="mt-3"
/>

4
src/components/Note/Zap.tsx

@ -183,10 +183,10 @@ export default function Zap({ @@ -183,10 +183,10 @@ export default function Zap({
{comment ? (
<SuperchatCommentMarkdown event={event} comment={comment} className="mt-2" />
) : null}
{!isProfileWall ? (
{isNotification ? (
<TurnIntoSuperchatButton
event={event}
prominent={isNotification}
prominent
attestationRecipientPubkey={attestationRecipientPubkey}
className="mt-3"
/>

53
src/components/NoteList/index.tsx

@ -602,6 +602,21 @@ function tightestSinceFromSpellFilters(shardFilters: Filter[]): number | undefin @@ -602,6 +602,21 @@ function tightestSinceFromSpellFilters(shardFilters: Filter[]): number | undefin
return sinceCandidates.length > 0 ? Math.max(...sinceCandidates) : undefined
}
/** `#p` mention filters (notifications spell) — used for fast IndexedDB payment-notification warm-up. */
function recipientPubkeysFromSpellFilters(shardFilters: Filter[]): string[] {
const out = new Set<string>()
for (const f of shardFilters) {
const pTags = f['#p']
if (!Array.isArray(pTags)) continue
for (const pk of pTags) {
if (typeof pk !== 'string') continue
const norm = pk.trim().toLowerCase()
if (/^[0-9a-f]{64}$/.test(norm)) out.add(norm)
}
}
return [...out]
}
/** Union of `filter.kinds` across mapped REQ shards; empty if any shard omits kinds (caller should not use fallback). */
function filterEvsToMappedTimelineReqKinds(
evs: Event[],
@ -2571,6 +2586,44 @@ const NoteList = forwardRef( @@ -2571,6 +2586,44 @@ const NoteList = forwardRef(
urls: string[]
filter: TSubRequestFilter
}>
const mergeSpellLocalDiskLayer = (incoming: Event[], variant: string) => {
if (!effectActive || timelineEffectStale()) return
const narrowed = narrowLiveBatch(incoming)
if (narrowed.length === 0) return
const merged = collapseDuplicateNip18RepostTimelineRows(
mergeEventBatchesById(spellLocalMergeBase, narrowed, eventCapEarly, areAlgoRelays)
)
if (merged.length === 0) return
spellLocalMergeBase = merged
timelineMergeBootstrapRef.current = merged.slice()
setEvents(merged)
lastEventsForTimelinePrefetchRef.current = merged
setNewEvents([])
setShowCount(revealBatchSize ?? SHOW_COUNT)
setLoading(false)
feedPaintRelayPendingRef.current = true
feedPaintRelayMetaRef.current = { variant, mergedCount: merged.length }
setFeedEmptyToastGateTick((n) => n + 1)
setFeedTimelineEmptyUiReady(true)
}
const mentionRecipients = recipientPubkeysFromSpellFilters(shardFilters)
if (mentionRecipients.length === 1) {
try {
const paymentNotifications = await indexedDb.getPaymentNotificationsForRecipient(
mentionRecipients[0]!,
localLayerCap
)
mergeSpellLocalDiskLayer(
paymentNotifications.filter(matchesSpellLocal),
'spell_payment_notifications_idb'
)
} catch {
/* best-effort */
}
}
const [diskRaw, filterAwareLocalRaw, fromPub, fromArch] = await Promise.all([
client.getTimelineDiskSnapshotEvents(filterAwareDiskReq),
client.getLocalFeedEvents(filterAwareDiskReq, {

2
src/components/TurnIntoSuperchatButton/index.tsx

@ -84,7 +84,7 @@ function TurnIntoSuperchatButtonInner({ @@ -84,7 +84,7 @@ function TurnIntoSuperchatButtonInner({
className
)}
>
{t('Superchat attested')}
{t('Confirmed by recipient')}
</p>
)
}

24
src/hooks/usePaymentAttestationStatus.test.ts

@ -1,6 +1,10 @@ @@ -1,6 +1,10 @@
import { ExtendedKind } from '@/constants'
import {
markLocalAttestationTarget,
rememberPaymentAttestation
} from '@/lib/payment-attestation-cache'
import { describe, expect, it } from 'vitest'
import { isPaymentAttestationForTarget } from './usePaymentAttestationStatus'
import { isPaymentAttestationForTarget, readAttestedFromLocalSources } from './usePaymentAttestationStatus'
import type { Event } from 'nostr-tools'
const recipient = 'a'.repeat(64)
@ -19,6 +23,24 @@ function attestationEvent(overrides: Partial<Event> = {}): Event { @@ -19,6 +23,24 @@ function attestationEvent(overrides: Partial<Event> = {}): Event {
}
}
describe('readAttestedFromLocalSources', () => {
it('returns attested when the in-memory cache has a match', () => {
const attestation = attestationEvent()
rememberPaymentAttestation(targetId, recipient, attestation)
const result = readAttestedFromLocalSources(targetId, recipient)
expect(result.attested).toBe(true)
expect(result.attestationEvent?.id).toBe(attestation.id)
})
it('returns attested when durable local storage marks the target', () => {
const otherTargetId = 'd'.repeat(64)
markLocalAttestationTarget(recipient, otherTargetId)
const result = readAttestedFromLocalSources(otherTargetId, recipient)
expect(result.attested).toBe(true)
expect(result.attestationEvent).toBeNull()
})
})
describe('isPaymentAttestationForTarget', () => {
it('accepts a matching kind 9741 attestation', () => {
expect(isPaymentAttestationForTarget(attestationEvent(), targetId, recipient)).toBe(true)

100
src/hooks/usePaymentAttestationStatus.tsx

@ -3,13 +3,16 @@ import { @@ -3,13 +3,16 @@ import {
getPaymentAttestationTargetId,
getSuperchatPaymentRecipientPubkey
} from '@/lib/superchat'
import { hexPubkeysEqual } from '@/lib/pubkey'
import { hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey'
import {
hydrateAttestationsForAuthor,
isLocallyMarkedAttested,
loadPaymentAttestationLocal,
peekCachedPaymentAttestation,
readKnownAttestedPaymentTargetsSync,
refreshPaymentAttestationFromRelays,
rememberPaymentAttestationFromPublish
rememberPaymentAttestationFromPublish,
resolveAttestedPaymentIdSet
} from '@/lib/payment-attestation-cache'
import client from '@/services/client.service'
import { Event as NostrEvent } from 'nostr-tools'
@ -35,27 +38,45 @@ export function isPaymentAttestationForTarget( @@ -35,27 +38,45 @@ export function isPaymentAttestationForTarget(
return Boolean(attestedId && attestedId.toLowerCase() === targetEventId.trim().toLowerCase())
}
function readAttestedFromLocalSources(
export function readAttestedFromLocalSources(
targetEventId: string | undefined,
recipientPubkey: string | null
): { attested: boolean; attestationEvent: NostrEvent | null } {
if (!targetEventId || !recipientPubkey) {
return { attested: false, attestationEvent: null }
}
const targetLower = targetEventId.trim().toLowerCase()
const hit = peekCachedPaymentAttestation(targetEventId, recipientPubkey)
return {
attested: Boolean(hit),
attestationEvent: hit ?? null
if (hit) {
return { attested: true, attestationEvent: hit }
}
if (
isLocallyMarkedAttested(recipientPubkey, targetEventId) ||
readKnownAttestedPaymentTargetsSync(recipientPubkey).has(targetLower)
) {
return { attested: true, attestationEvent: null }
}
return { attested: false, attestationEvent: null }
}
function isTargetInAttestedSet(
attestedIds: ReadonlySet<string>,
targetEventId: string
): boolean {
return attestedIds.has(targetEventId.trim().toLowerCase())
}
export function usePaymentAttestationStatus(
targetEvent: NostrEvent | undefined,
recipientPubkeyOverride?: string | null
) {
const recipientPubkey = targetEvent
? recipientPubkeyOverride ?? getSuperchatPaymentRecipientPubkey(targetEvent)
: null
const recipientPubkey = useMemo(() => {
if (!targetEvent) return null
const raw = recipientPubkeyOverride ?? getSuperchatPaymentRecipientPubkey(targetEvent)
if (!raw) return null
const normalized = normalizeHexPubkey(raw)
return /^[0-9a-f]{64}$/.test(normalized) ? normalized : null
}, [recipientPubkeyOverride, targetEvent])
const targetId = targetEvent?.id?.toLowerCase()
const filter = useMemo(
@ -108,10 +129,43 @@ export function usePaymentAttestationStatus( @@ -108,10 +129,43 @@ export function usePaymentAttestationStatus(
setAttested(next.attested)
}, [recipientPubkey, targetEvent?.id, targetId])
const applyResolvedAttestation = useCallback(
(attestedIds: ReadonlySet<string>) => {
if (!targetEvent?.id || !recipientPubkey) return false
if (!isTargetInAttestedSet(attestedIds, targetEvent.id)) return false
const cached = peekCachedPaymentAttestation(targetEvent.id, recipientPubkey)
if (cached) {
applyMatch(cached)
} else {
setAttestationEvent(null)
setAttested(true)
}
return true
},
[applyMatch, recipientPubkey, targetEvent?.id]
)
useEffect(() => {
if (!recipientPubkey) return
void hydrateAttestationsForAuthor(recipientPubkey)
}, [recipientPubkey])
if (!recipientPubkey || !targetEvent?.id) return
let cancelled = false
void (async () => {
await hydrateAttestationsForAuthor(recipientPubkey)
if (cancelled) return
const next = readAttestedFromLocalSources(targetEvent.id, recipientPubkey)
if (!next.attested) return
if (next.attestationEvent) {
applyMatch(next.attestationEvent)
} else {
setAttestationEvent(null)
setAttested(true)
}
})()
return () => {
cancelled = true
}
}, [applyMatch, recipientPubkey, targetEvent?.id, targetId])
useEffect(() => {
if (!targetEvent?.id || !recipientPubkey || !filter) return
@ -127,6 +181,11 @@ export function usePaymentAttestationStatus( @@ -127,6 +181,11 @@ export function usePaymentAttestationStatus(
applyMatch(local)
return
}
const attestedIds = await resolveAttestedPaymentIdSet(recipientPubkey)
if (cancelled) return
if (applyResolvedAttestation(attestedIds)) return
const relay = await refreshPaymentAttestationFromRelays(
targetEvent.id,
recipientPubkey,
@ -135,7 +194,12 @@ export function usePaymentAttestationStatus( @@ -135,7 +194,12 @@ export function usePaymentAttestationStatus(
if (cancelled) return
if (relay) {
applyMatch(relay)
} else {
return
}
const afterRelay = await resolveAttestedPaymentIdSet(recipientPubkey)
if (cancelled) return
if (!applyResolvedAttestation(afterRelay)) {
clearAttested()
}
} catch {
@ -148,7 +212,15 @@ export function usePaymentAttestationStatus( @@ -148,7 +212,15 @@ export function usePaymentAttestationStatus(
return () => {
cancelled = true
}
}, [applyMatch, clearAttested, filter, recipientPubkey, targetEvent?.id, targetId])
}, [
applyMatch,
applyResolvedAttestation,
clearAttested,
filter,
recipientPubkey,
targetEvent?.id,
targetId
])
useEffect(() => {
if (!targetEvent?.id || !recipientPubkey) return

1
src/i18n/locales/en.ts

@ -221,6 +221,7 @@ export default { @@ -221,6 +221,7 @@ export default {
"Invalid superchat": "Invalid superchat",
"Turn this into a superchat!": "Turn this into a superchat!",
"Superchat attested": "Superchat attested",
"Confirmed by recipient": "Confirmed by recipient",
"Failed to attest superchat": "Failed to attest superchat: {{error}}",
"Superchat thread": "View thread",
"Superchat profile": "View profile",

4
src/lib/payment-attestation-cache.ts

@ -192,10 +192,6 @@ export async function refreshPaymentAttestationFromRelays( @@ -192,10 +192,6 @@ export async function refreshPaymentAttestationFromRelays(
const cached = peekCachedPaymentAttestation(targetEventId, recipientPubkey)
if (cached) return cached
if (isLocallyMarkedAttested(recipientPubkey, targetEventId)) {
return undefined
}
const key = paymentAttestationCacheKey(targetEventId, recipientPubkey)
let inflight = relayFetchByTargetKey.get(key)
if (!inflight) {

15
src/services/client.service.ts

@ -2267,22 +2267,25 @@ class ClientService extends EventTarget { @@ -2267,22 +2267,25 @@ class ClientService extends EventTarget {
add(this.eventService.getSessionEventsMatchingFilters(filters, maxMatches))
const [timelineRows, archiveRows, publicationRows, paymentSuperchatRows] = await Promise.all([
const [timelineRows, paymentSuperchatRows] = await Promise.all([
this.getTimelineDiskSnapshotEvents(subRequests).catch(() => [] as NEvent[]),
indexedDb
.getPaymentSuperchatEventsMatchingFilters(filters, maxMatches)
.catch(() => [] as NEvent[])
])
add(timelineRows)
add(paymentSuperchatRows)
const [archiveRows, publicationRows] = await Promise.all([
indexedDb
.scanEventArchiveByFilters(filters, { maxRowsScanned, maxMatches })
.catch(() => [] as NEvent[]),
indexedDb
.scanPublicationEventsByFilters(filters, { maxRowsScanned: Math.min(maxRowsScanned, 16_000), maxMatches })
.catch(() => [] as NEvent[]),
indexedDb
.getPaymentSuperchatEventsMatchingFilters(filters, maxMatches)
.catch(() => [] as NEvent[])
])
add(timelineRows)
add(archiveRows)
add(publicationRows)
add(paymentSuperchatRows)
return [...byId.values()]
.sort((a, b) => b.created_at - a.created_at || b.id.localeCompare(a.id))

Loading…
Cancel
Save