From 2f4f4fffcf68f1521bf9c1a69d93374bad1af628 Mon Sep 17 00:00:00 2001 From: codytseng Date: Thu, 7 Aug 2025 00:24:03 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=90=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/NoteList/index.tsx | 20 +++++++++++++---- src/components/ReplyNoteList/index.tsx | 6 ++--- src/lib/draft-event.ts | 4 ++-- src/lib/event.ts | 31 ++++++++++++++++++++++++-- src/services/client.service.ts | 27 ++++++++++++++++++++-- 5 files changed, 75 insertions(+), 13 deletions(-) diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index 3aa3134..82d0040 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -1,7 +1,11 @@ import NewNotesButton from '@/components/NewNotesButton' import { Button } from '@/components/ui/button' 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 { isSafari } from '@/lib/utils' import { useMuteList } from '@/providers/MuteListProvider' @@ -296,6 +300,8 @@ export default function NoteList({ }, 0) } + const idSet = new Set() + return (
{events .slice(0, showCount) - .filter( - (event: Event) => + .filter((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)) && (skipTrustCheck || !hideUntrustedNotes || isUserTrusted(event.pubkey)) - ) + ) + }) .map((event) => ( () const replyEvents: NEvent[] = [] const currentEventKey = isReplaceableEvent(event.kind) - ? getReplaceableEventCoordinate(event) + ? getReplaceableCoordinateFromEvent(event) : event.id let parentEventKeys = [currentEventKey] while (parentEventKeys.length > 0) { @@ -64,7 +64,7 @@ export default function ReplyNoteList({ index, event }: { index?: number; event: let root: TRootInfo = isReplaceableEvent(event.kind) ? { type: 'A', - id: getReplaceableEventCoordinate(event), + id: getReplaceableCoordinateFromEvent(event), eventId: event.id, pubkey: event.pubkey, relay: client.getEventHint(event.id) diff --git a/src/lib/draft-event.ts b/src/lib/draft-event.ts index b43c086..133ada9 100644 --- a/src/lib/draft-event.ts +++ b/src/lib/draft-event.ts @@ -12,7 +12,7 @@ import { import dayjs from 'dayjs' import { Event, kinds, nip19 } from 'nostr-tools' import { - getReplaceableEventCoordinate, + getReplaceableCoordinateFromEvent, getRootETag, isProtectedEvent, isReplaceableEvent @@ -552,7 +552,7 @@ function extractImagesFromContent(content: string) { } function buildATag(event: Event, upperCase: boolean = false) { - const coordinate = getReplaceableEventCoordinate(event) + const coordinate = getReplaceableCoordinateFromEvent(event) const hint = client.getEventHint(event.id) return trimTagEnd([upperCase ? 'A' : 'a', coordinate, hint]) } diff --git a/src/lib/event.ts b/src/lib/event.ts index b72f65b..64e1b12 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -149,9 +149,13 @@ export function getRootBech32Id(event?: Event) { 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] - return `${event.kind}:${event.pubkey}:${d ?? ''}` + return getReplaceableCoordinate(event.kind, event.pubkey, d) } export function getNoteBech32Id(event: Event) { @@ -242,3 +246,26 @@ export function createFakeEvent(event: Partial): 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 +} diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 56570e5..28867a5 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -1,5 +1,11 @@ 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 { formatPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey' import { getPubkeysFromPTags, getServersFromServerTags } from '@/lib/tag' @@ -42,6 +48,7 @@ class ClientService extends EventTarget { | string[] | undefined > = {} + private replaceableEventCacheMap = new Map() private eventCacheMap = new Map>() private eventDataLoader = new DataLoader( (ids) => Promise.all(ids.map((id) => this._fetchEvent(id))), @@ -438,6 +445,13 @@ class ClientService extends EventTarget { startLogin, onevent: (evt: NEvent) => { 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 if (!eosedAt) { return events.push(evt) @@ -635,6 +649,7 @@ class ClientService extends EventTarget { async fetchEvent(id: string): Promise { if (!/^[0-9a-f]{64}$/.test(id)) { let eventId: string | undefined + let coordinate: string | undefined const { type, data } = nip19.decode(id) switch (type) { case 'note': @@ -643,8 +658,16 @@ class ClientService extends EventTarget { case 'nevent': eventId = data.id 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) if (cache) { return cache