Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
a3bb50e769
  1. 23
      src/components/Embedded/EmbeddedNote.tsx
  2. 10
      src/components/PostEditor/PostRelaySelector.tsx
  3. 28
      src/components/ReplyNoteList/index.tsx
  4. 6
      src/constants.ts
  5. 11
      src/contexts/suppress-embedded-note-context.tsx
  6. 44
      src/lib/event.ts

23
src/components/Embedded/EmbeddedNote.tsx

@ -42,6 +42,20 @@ function hexEventIdFromNoteId(noteId: string): string | null {
} }
} }
/** For naddr (replaceable events), return coordinate kind:pubkey:identifier for suppression matching. */
function coordinateFromNoteId(noteId: string): string | null {
try {
const { type, data } = nip19.decode(noteId.trim())
if (type === 'naddr' && data) {
const id = data.identifier ?? ''
return `${data.kind}:${data.pubkey}:${id}`.toLowerCase()
}
return null
} catch {
return null
}
}
/** True if `fetchEventWithExternalRelays(noteId, …)` can build a REQ filter (hex, note, nevent, naddr). */ /** True if `fetchEventWithExternalRelays(noteId, …)` can build a REQ filter (hex, note, nevent, naddr). */
function canSearchOnExternalRelays(noteId: string): boolean { function canSearchOnExternalRelays(noteId: string): boolean {
if (hexEventIdFromNoteId(noteId)) return true if (hexEventIdFromNoteId(noteId)) return true
@ -61,10 +75,13 @@ export function EmbeddedNote({
className?: string className?: string
containingEvent?: Event containingEvent?: Event
}) { }) {
const suppressId = useSuppressEmbeddedNoteId() const suppress = useSuppressEmbeddedNoteId()
const embeddedHexId = useMemo(() => hexEventIdFromNoteId(noteId), [noteId]) const embeddedHexId = useMemo(() => hexEventIdFromNoteId(noteId), [noteId])
if (suppressId && embeddedHexId && embeddedHexId === suppressId.toLowerCase()) { const embeddedCoordinate = useMemo(() => coordinateFromNoteId(noteId), [noteId])
return null if (suppress) {
if (embeddedHexId && embeddedHexId === suppress.hexId.toLowerCase()) return null
if (suppress.coordinate && embeddedCoordinate && embeddedCoordinate === suppress.coordinate.toLowerCase())
return null
} }
const validation = useMemo(() => validateEmbeddedNotePointer(noteId), [noteId]) const validation = useMemo(() => validateEmbeddedNotePointer(noteId), [noteId])
if (!validation.valid) { if (!validation.valid) {

10
src/components/PostEditor/PostRelaySelector.tsx

@ -1,3 +1,4 @@
import { KIND_1_BLOCKED_RELAY_URLS } from '@/constants'
import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns' import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns'
import { simplifyUrl, isLocalNetworkUrl, normalizeUrl } from '@/lib/url' import { simplifyUrl, isLocalNetworkUrl, normalizeUrl } from '@/lib/url'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
@ -92,7 +93,14 @@ export default function PostRelaySelector({
// Memoize arrays to prevent unnecessary re-renders // Memoize arrays to prevent unnecessary re-renders
const memoizedFavoriteRelays = useMemo(() => favoriteRelays, [favoriteRelays]) const memoizedFavoriteRelays = useMemo(() => favoriteRelays, [favoriteRelays])
const memoizedBlockedRelays = useMemo(() => blockedRelays, [blockedRelays]) const memoizedBlockedRelays = useMemo(() => {
// For kind 1 replies and top-level posts, also block KIND_1_BLOCKED_RELAY_URLS
const isKind1Publish =
!isPublicMessage && (typeof _parentEvent?.kind === 'undefined' || _parentEvent?.kind === 1)
return isKind1Publish
? [...blockedRelays, ...KIND_1_BLOCKED_RELAY_URLS]
: blockedRelays
}, [blockedRelays, isPublicMessage, _parentEvent?.kind])
const memoizedRelaySets = useMemo(() => relaySets, [relaySets]) const memoizedRelaySets = useMemo(() => relaySets, [relaySets])
const memoizedOpenFrom = useMemo(() => openFrom, [openFrom]) const memoizedOpenFrom = useMemo(() => openFrom, [openFrom])

28
src/components/ReplyNoteList/index.tsx

@ -209,6 +209,8 @@ function ReplyNoteList({
}, [event.id, repliesMap, mutePubkeySet, hideContentMentioningMutedUsers, sort]) }, [event.id, repliesMap, mutePubkeySet, hideContentMentioningMutedUsers, sort])
const replyIdSet = useMemo(() => new Set(replies.map((r) => r.id)), [replies]) const replyIdSet = useMemo(() => new Set(replies.map((r) => r.id)), [replies])
/** Events that quote the note (from useQuoteEvents) — render with quote styling and without embedded quote. */
const quoteIdSet = useMemo(() => new Set(quoteEvents.map((e) => e.id)), [quoteEvents])
const mergedFeed = useMemo(() => { const mergedFeed = useMemo(() => {
if (!showQuotes) return replies if (!showQuotes) return replies
const quoteOnly = quoteEvents.filter((e) => !replyIdSet.has(e.id)) const quoteOnly = quoteEvents.filter((e) => !replyIdSet.has(e.id))
@ -383,18 +385,16 @@ function ReplyNoteList({
const threadRelayHints = [ const threadRelayHints = [
...new Set([...relayHintsFromEventTags(event), ...seenOn, ...fromBrowsingFeed]) ...new Set([...relayHintsFromEventTags(event), ...seenOn, ...fromBrowsingFeed])
] ]
let finalRelayUrls = await buildReplyReadRelayList( const replyBlockedRelays = [
...(blockedRelays || []),
...E_TAG_FILTER_BLOCKED_RELAY_URLS
]
const finalRelayUrls = await buildReplyReadRelayList(
opAuthorPubkey, opAuthorPubkey,
userPubkey || undefined, userPubkey || undefined,
blockedRelays || [], replyBlockedRelays,
threadRelayHints threadRelayHints
) )
const eTagBlockedSet = new Set(
E_TAG_FILTER_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u)
)
finalRelayUrls = finalRelayUrls.filter(
(u) => !eTagBlockedSet.has(normalizeUrl(u) || u)
)
const filters: Filter[] = [] const filters: Filter[] = []
if (rootInfo.type === 'E') { if (rootInfo.type === 'E') {
@ -585,7 +585,7 @@ function ReplyNoteList({
)} )}
<div> <div>
{mergedFeed.slice(0, showCount).map((item) => { {mergedFeed.slice(0, showCount).map((item) => {
const isQuote = !replyIdSet.has(item.id) const isQuote = quoteIdSet.has(item.id)
// Don't filter by trust until trust data is loaded - prevents replies from // Don't filter by trust until trust data is loaded - prevents replies from
// vanishing when wotSet is still empty (all non-self appear untrusted) // vanishing when wotSet is still empty (all non-self appear untrusted)
if (isTrustLoaded && hideUntrustedInteractions && !isUserTrusted(item.pubkey)) { if (isTrustLoaded && hideUntrustedInteractions && !isUserTrusted(item.pubkey)) {
@ -606,9 +606,15 @@ function ReplyNoteList({
: item.kind === kinds.LongFormArticle : item.kind === kinds.LongFormArticle
? t('cited in article') ? t('cited in article')
: t('quoted this note') : t('quoted this note')
const hideQuotedNote = eventReferencesEventId(item, event.id) const hideQuotedNote = eventReferencesEventId(item, event)
return ( return (
<SuppressEmbeddedNoteContext.Provider key={item.id} value={event.id}> <SuppressEmbeddedNoteContext.Provider
key={item.id}
value={{
hexId: event.id,
coordinate: isReplaceableEvent(event.kind) ? getReplaceableCoordinateFromEvent(event) : undefined
}}
>
<div <div
ref={(el) => (replyRefs.current[item.id] = el)} ref={(el) => (replyRefs.current[item.id] = el)}
className="scroll-mt-12 border-l-2 border-muted-foreground/40 pl-3 py-1 my-1 rounded-r" className="scroll-mt-12 border-l-2 border-muted-foreground/40 pl-3 py-1 my-1 rounded-r"

6
src/constants.ts

@ -157,6 +157,12 @@ export const BOOKSTR_RELAY_URLS = [
'wss://orly-relay.imwald.eu' 'wss://orly-relay.imwald.eu'
] ]
/**
* Block-list order (applied in sequence when building relay lists):
* 1. READ_ONLY never publish
* 2. KIND_1_BLOCKED skip for kind 1 read/write
* 3. E_TAG_FILTER_BLOCKED skip for reply/quote/stats fetches (#e, #a, #q filters)
*/
/** Relays that must never be used for publishing (read-only aggregators, etc.). */ /** Relays that must never be used for publishing (read-only aggregators, etc.). */
export const READ_ONLY_RELAY_URLS = ['wss://aggr.nostr.land'] export const READ_ONLY_RELAY_URLS = ['wss://aggr.nostr.land']

11
src/contexts/suppress-embedded-note-context.tsx

@ -1,8 +1,13 @@
import { createContext, useContext } from 'react' import { createContext, useContext } from 'react'
/** When set, EmbeddedNote should not render notes whose id matches this (avoids redundancy when viewing "quotes of this note"). */ export type SuppressEmbeddedNoteValue = {
export const SuppressEmbeddedNoteContext = createContext<string | undefined>(undefined) hexId: string
coordinate?: string
}
/** When set, EmbeddedNote should not render notes whose id/coordinate matches (avoids redundancy when viewing "quotes of this note"). */
export const SuppressEmbeddedNoteContext = createContext<SuppressEmbeddedNoteValue | undefined>(undefined)
export function useSuppressEmbeddedNoteId(): string | undefined { export function useSuppressEmbeddedNoteId(): SuppressEmbeddedNoteValue | undefined {
return useContext(SuppressEmbeddedNoteContext) return useContext(SuppressEmbeddedNoteContext)
} }

44
src/lib/event.ts

@ -199,18 +199,38 @@ export function getRootEventHexId(event?: Event) {
return tag?.[1] return tag?.[1]
} }
/** True if event references targetHexId as root, parent, or quoted (#q) — used to hide redundant preview when showing quotes of current note. */ /** True if event references target as root, parent, or quoted (#q, #a) — used to hide redundant preview when showing quotes of current note. */
export function eventReferencesEventId(event: Event | undefined, targetHexId: string): boolean { export function eventReferencesEventId(
if (!event || !targetHexId) return false event: Event | undefined,
const target = targetHexId.toLowerCase() targetHexIdOrEvent: string | Event
const rootId = getRootETag(event)?.[1]?.toLowerCase() ): boolean {
if (rootId === target) return true if (!event) return false
const parentId = getParentETag(event)?.[1]?.toLowerCase() const targetEvent = typeof targetHexIdOrEvent === 'object' ? targetHexIdOrEvent : undefined
if (parentId === target) return true const targetHexId =
const qTag = event.tags.find((t) => t[0] === 'q' || t[0] === 'Q')?.[1]?.toLowerCase() typeof targetHexIdOrEvent === 'string'
if (qTag === target) return true ? targetHexIdOrEvent.toLowerCase()
const eTags = event.tags.filter((t) => t[0] === 'e' || t[0] === 'E') : targetHexIdOrEvent.id?.toLowerCase()
if (eTags.some((t) => t[1]?.toLowerCase() === target)) return true const targetCoordinate =
targetEvent && isReplaceableEvent(targetEvent.kind)
? getReplaceableCoordinateFromEvent(targetEvent)
: undefined
if (targetHexId) {
const rootId = getRootETag(event)?.[1]?.toLowerCase()
if (rootId === targetHexId) return true
const parentId = getParentETag(event)?.[1]?.toLowerCase()
if (parentId === targetHexId) return true
const qTag = event.tags.find((t) => t[0] === 'q' || t[0] === 'Q')?.[1]?.toLowerCase()
if (qTag === targetHexId) return true
const eTags = event.tags.filter((t) => t[0] === 'e' || t[0] === 'E')
if (eTags.some((t) => t[1]?.toLowerCase() === targetHexId)) return true
}
if (targetCoordinate) {
const aTags = event.tags.filter((t) => t[0] === 'a' || t[0] === 'A')
if (aTags.some((t) => t[1]?.toLowerCase() === targetCoordinate.toLowerCase())) return true
}
return false return false
} }

Loading…
Cancel
Save