fix: 🐛

This commit is contained in:
codytseng
2025-08-07 00:24:03 +08:00
parent 83052a2ba8
commit 2f4f4fffcf
5 changed files with 75 additions and 13 deletions

View File

@@ -1,7 +1,11 @@
import NewNotesButton from '@/components/NewNotesButton' import NewNotesButton from '@/components/NewNotesButton'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { isReplyNoteEvent } from '@/lib/event' import {
getReplaceableCoordinateFromEvent,
isReplaceableEvent,
isReplyNoteEvent
} from '@/lib/event'
import { checkAlgoRelay } from '@/lib/relay' import { checkAlgoRelay } from '@/lib/relay'
import { isSafari } from '@/lib/utils' import { isSafari } from '@/lib/utils'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
@@ -296,6 +300,8 @@ export default function NoteList({
}, 0) }, 0)
} }
const idSet = new Set<string>()
return ( return (
<div className={className}> <div className={className}>
<Tabs <Tabs
@@ -338,11 +344,17 @@ export default function NoteList({
<div className="min-h-screen"> <div className="min-h-screen">
{events {events
.slice(0, showCount) .slice(0, showCount)
.filter( .filter((event: Event) => {
(event: Event) => const id = isReplaceableEvent(event.kind)
? getReplaceableCoordinateFromEvent(event)
: event.id
if (idSet.has(id)) return false
idSet.add(id)
return (
(listMode !== 'posts' || !isReplyNoteEvent(event)) && (listMode !== 'posts' || !isReplyNoteEvent(event)) &&
(skipTrustCheck || !hideUntrustedNotes || isUserTrusted(event.pubkey)) (skipTrustCheck || !hideUntrustedNotes || isUserTrusted(event.pubkey))
) )
})
.map((event) => ( .map((event) => (
<NoteCard <NoteCard
key={event.id} key={event.id}

View File

@@ -1,7 +1,7 @@
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { import {
getParentETag, getParentETag,
getReplaceableEventCoordinate, getReplaceableCoordinateFromEvent,
getRootATag, getRootATag,
getRootETag, getRootETag,
getRootEventHexId, getRootEventHexId,
@@ -37,7 +37,7 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
const replyIdSet = new Set<string>() const replyIdSet = new Set<string>()
const replyEvents: NEvent[] = [] const replyEvents: NEvent[] = []
const currentEventKey = isReplaceableEvent(event.kind) const currentEventKey = isReplaceableEvent(event.kind)
? getReplaceableEventCoordinate(event) ? getReplaceableCoordinateFromEvent(event)
: event.id : event.id
let parentEventKeys = [currentEventKey] let parentEventKeys = [currentEventKey]
while (parentEventKeys.length > 0) { while (parentEventKeys.length > 0) {
@@ -64,7 +64,7 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
let root: TRootInfo = isReplaceableEvent(event.kind) let root: TRootInfo = isReplaceableEvent(event.kind)
? { ? {
type: 'A', type: 'A',
id: getReplaceableEventCoordinate(event), id: getReplaceableCoordinateFromEvent(event),
eventId: event.id, eventId: event.id,
pubkey: event.pubkey, pubkey: event.pubkey,
relay: client.getEventHint(event.id) relay: client.getEventHint(event.id)

View File

@@ -12,7 +12,7 @@ import {
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Event, kinds, nip19 } from 'nostr-tools' import { Event, kinds, nip19 } from 'nostr-tools'
import { import {
getReplaceableEventCoordinate, getReplaceableCoordinateFromEvent,
getRootETag, getRootETag,
isProtectedEvent, isProtectedEvent,
isReplaceableEvent isReplaceableEvent
@@ -552,7 +552,7 @@ function extractImagesFromContent(content: string) {
} }
function buildATag(event: Event, upperCase: boolean = false) { function buildATag(event: Event, upperCase: boolean = false) {
const coordinate = getReplaceableEventCoordinate(event) const coordinate = getReplaceableCoordinateFromEvent(event)
const hint = client.getEventHint(event.id) const hint = client.getEventHint(event.id)
return trimTagEnd([upperCase ? 'A' : 'a', coordinate, hint]) return trimTagEnd([upperCase ? 'A' : 'a', coordinate, hint])
} }

View File

@@ -149,9 +149,13 @@ export function getRootBech32Id(event?: Event) {
return generateBech32IdFromETag(eTag) return generateBech32IdFromETag(eTag)
} }
export function getReplaceableEventCoordinate(event: Event) { export function getReplaceableCoordinate(kind: number, pubkey: string, d: string = '') {
return `${kind}:${pubkey}:${d}`
}
export function getReplaceableCoordinateFromEvent(event: Event) {
const d = event.tags.find(tagNameEquals('d'))?.[1] const d = event.tags.find(tagNameEquals('d'))?.[1]
return `${event.kind}:${event.pubkey}:${d ?? ''}` return getReplaceableCoordinate(event.kind, event.pubkey, d)
} }
export function getNoteBech32Id(event: Event) { export function getNoteBech32Id(event: Event) {
@@ -242,3 +246,26 @@ export function createFakeEvent(event: Partial<Event>): Event {
...event ...event
} }
} }
// Legacy compare function for sorting compatibility
// If return 0, it means the two events are equal.
// If return a negative number, it means `b` should be retained, and `a` should be discarded.
// If return a positive number, it means `a` should be retained, and `b` should be discarded.
export function compareEvents(a: Event, b: Event): number {
if (a.created_at !== b.created_at) {
return a.created_at - b.created_at
}
// In case of replaceable events with the same timestamp, the event with the lowest id (first in lexical order) should be retained, and the other discarded.
if (a.id !== b.id) {
return a.id < b.id ? 1 : -1
}
return 0
}
// Returns the event that should be retained when comparing two events
export function getRetainedEvent(a: Event, b: Event): Event {
if (compareEvents(a, b) > 0) {
return a
}
return b
}

View File

@@ -1,5 +1,11 @@
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { getLatestEvent } from '@/lib/event' import {
compareEvents,
getLatestEvent,
getReplaceableCoordinate,
getReplaceableCoordinateFromEvent,
isReplaceableEvent
} from '@/lib/event'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { formatPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey' import { formatPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey'
import { getPubkeysFromPTags, getServersFromServerTags } from '@/lib/tag' import { getPubkeysFromPTags, getServersFromServerTags } from '@/lib/tag'
@@ -42,6 +48,7 @@ class ClientService extends EventTarget {
| string[] | string[]
| undefined | undefined
> = {} > = {}
private replaceableEventCacheMap = new Map<string, NEvent>()
private eventCacheMap = new Map<string, Promise<NEvent | undefined>>() private eventCacheMap = new Map<string, Promise<NEvent | undefined>>()
private eventDataLoader = new DataLoader<string, NEvent | undefined>( private eventDataLoader = new DataLoader<string, NEvent | undefined>(
(ids) => Promise.all(ids.map((id) => this._fetchEvent(id))), (ids) => Promise.all(ids.map((id) => this._fetchEvent(id))),
@@ -438,6 +445,13 @@ class ClientService extends EventTarget {
startLogin, startLogin,
onevent: (evt: NEvent) => { onevent: (evt: NEvent) => {
that.eventDataLoader.prime(evt.id, Promise.resolve(evt)) that.eventDataLoader.prime(evt.id, Promise.resolve(evt))
if (isReplaceableEvent(evt.kind)) {
const coordinate = getReplaceableCoordinateFromEvent(evt)
const cachedEvent = that.replaceableEventCacheMap.get(coordinate)
if (!cachedEvent || compareEvents(evt, cachedEvent) > 0) {
that.replaceableEventCacheMap.set(coordinate, evt)
}
}
// not eosed yet, push to events // not eosed yet, push to events
if (!eosedAt) { if (!eosedAt) {
return events.push(evt) return events.push(evt)
@@ -635,6 +649,7 @@ class ClientService extends EventTarget {
async fetchEvent(id: string): Promise<NEvent | undefined> { async fetchEvent(id: string): Promise<NEvent | undefined> {
if (!/^[0-9a-f]{64}$/.test(id)) { if (!/^[0-9a-f]{64}$/.test(id)) {
let eventId: string | undefined let eventId: string | undefined
let coordinate: string | undefined
const { type, data } = nip19.decode(id) const { type, data } = nip19.decode(id)
switch (type) { switch (type) {
case 'note': case 'note':
@@ -643,8 +658,16 @@ class ClientService extends EventTarget {
case 'nevent': case 'nevent':
eventId = data.id eventId = data.id
break break
case 'naddr':
coordinate = getReplaceableCoordinate(data.kind, data.pubkey, data.identifier)
break
} }
if (eventId) { if (coordinate) {
const cache = this.replaceableEventCacheMap.get(coordinate)
if (cache) {
return cache
}
} else if (eventId) {
const cache = this.eventCacheMap.get(eventId) const cache = this.eventCacheMap.get(eventId)
if (cache) { if (cache) {
return cache return cache