From ca532b07f4d1ad31a2860a45096ef16fcc6f4cf3 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 26 May 2026 08:13:36 +0200 Subject: [PATCH] bug-fix stop using protected events with "-" tags --- src/components/KindFilter/index.tsx | 2 +- src/components/PostEditor/PollEditor.tsx | 3 - src/components/PostEditor/PostContent.tsx | 16 +----- .../PostEditor/PostRelaySelector.tsx | 15 ++--- src/lib/discussion-thread-composer.ts | 2 +- src/lib/draft-event.ts | 19 ------- src/services/relay-selection.service.ts | 56 ++++++++++--------- 7 files changed, 38 insertions(+), 75 deletions(-) diff --git a/src/components/KindFilter/index.tsx b/src/components/KindFilter/index.tsx index b3ee3174..75d72f6a 100644 --- a/src/components/KindFilter/index.tsx +++ b/src/components/KindFilter/index.tsx @@ -179,7 +179,7 @@ export default function KindFilter({ }} > - {t('Filter')} + {t('Filter')} {isDifferentFromSaved && (
)} diff --git a/src/components/PostEditor/PollEditor.tsx b/src/components/PostEditor/PollEditor.tsx index f17c0b7c..67ca555d 100644 --- a/src/components/PostEditor/PollEditor.tsx +++ b/src/components/PostEditor/PollEditor.tsx @@ -27,8 +27,6 @@ export default function PollEditor({ pollCreateData.endsAt ? dayjs(pollCreateData.endsAt * 1000).format('YYYY-MM-DDTHH:mm') : '' ) const [additionalRelayUrls, setAdditionalRelayUrls] = useState(pollCreateData.relays) - const [_isProtectedEvent, setIsProtectedEvent] = useState(false) - useEffect(() => { setPollCreateData({ isMultipleChoice, @@ -115,7 +113,6 @@ export default function PollEditor({
diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx index d3a6fec2..c7eec73d 100644 --- a/src/components/PostEditor/PostContent.tsx +++ b/src/components/PostEditor/PostContent.tsx @@ -99,7 +99,7 @@ import { TDraftEvent } from '@/types' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Switch } from '@/components/ui/switch' import { DISCUSSION_TOPICS } from '@/pages/primary/DiscussionsPage/discussionTopics' -import { getReplaceableCoordinateFromEvent, isProtectedEvent as isEventProtected, isReplaceableEvent, isReplyNoteEvent } from '@/lib/event' +import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event' import { Event, kinds } from 'nostr-tools' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -283,7 +283,6 @@ export default function PostContent({ const [extractedMentions, setExtractedMentions] = useState( initialPublicMessageTo ? [initialPublicMessageTo] : [] ) - const [isProtectedEvent, setIsProtectedEvent] = useState(false) const [additionalRelayUrls, setAdditionalRelayUrls] = useState([]) /** When set, too many relays are checked vs the per-publish cap; publish stays disabled until unchecking. */ const [relayCapBlockInfo, setRelayCapBlockInfo] = useState<{ @@ -644,7 +643,6 @@ export default function PostContent({ contentOk && (!isPoll || pollCreateData.options.filter((option) => !!option.trim()).length >= 2) && (!isPublicMessage || extractedMentions.length > 0 || parentEvent?.kind === ExtendedKind.PUBLIC_MESSAGE) && - (!isProtectedEvent || additionalRelayUrls.length > 0) && (!isHighlight || highlightData.sourceValue.trim() !== '') && (!isCitationInternal || !!citationInternalCTag.trim()) && (!isCitationExternal || (!!citationExternalUrl.trim() && !!citationAccessedOn.trim())) && @@ -665,7 +663,6 @@ export default function PostContent({ isPublicMessage, extractedMentions, parentEvent, - isProtectedEvent, additionalRelayUrls, isHighlight, highlightData, @@ -810,13 +807,6 @@ export default function PostContent({ const addExpirationTag = storage.getDefaultExpirationEnabled() const expirationMonths = storage.getDefaultExpirationMonths() - // Determine if we should use protected event tag - let shouldUseProtectedEvent = false - if (parentEvent) { - const isParentOP = !isReplyNoteEvent(parentEvent) - const parentHasProtectedTag = isEventProtected(parentEvent) - shouldUseProtectedEvent = isParentOP && parentHasProtectedTag - } // Public messages - check BEFORE media notes to ensure PMs with media stay as PMs if (isPublicMessage) { @@ -874,7 +864,6 @@ export default function PostContent({ mentions, { addClientTag, - protectedEvent: shouldUseProtectedEvent, isNsfw, addExpirationTag: addExpirationTag && isChattingKind(ExtendedKind.VOICE_COMMENT), expirationMonths, @@ -1106,7 +1095,6 @@ export default function PostContent({ if (parentEvent && parentEvent.kind !== kinds.ShortTextNote) { return await createCommentDraftEvent(cleanedText, parentEvent, mentions, { addClientTag, - protectedEvent: shouldUseProtectedEvent, isNsfw, addExpirationTag: addExpirationTag && isChattingKind(ExtendedKind.COMMENT), expirationMonths, @@ -1129,7 +1117,6 @@ export default function PostContent({ return await createShortTextNoteDraftEvent(cleanedText, mentions, { parentEvent, addClientTag, - protectedEvent: shouldUseProtectedEvent, isNsfw, addExpirationTag: addExpirationTag && isChattingKind(kinds.ShortTextNote), expirationMonths, @@ -3370,7 +3357,6 @@ export default function PostContent({ )} > > setAdditionalRelayUrls: Dispatch> /** Notifies the post form when the relay cap prevents honoring every checked relay (so the form can disable publish and show a banner). */ onRelayPublishCapChange?: (preview: TPrePublishRelayCapPreview) => void @@ -81,6 +79,8 @@ export default function PostRelaySelector({ const [description, setDescription] = useState('') const [isLoading, setIsLoading] = useState(true) const [hasManualSelection, setHasManualSelection] = useState(false) + /** Auto-picked relays from {@link relaySelectionService}; used to detect manual relay-picker changes. */ + const autoSelectedRelayUrlsRef = useRef([]) const [previousSelectableCount, setPreviousSelectableCount] = useState(0) // Generation counter: incremented every time the effect fires; async callback checks whether // it's still the latest invocation before committing state, preventing stale races. @@ -201,6 +201,7 @@ export default function PostRelaySelector({ const cacheRelays = result.selectableRelays.filter(url => isLocalNetworkUrl(url)) const selectedWithCache = Array.from(new Set([...result.selectedRelays, ...cacheRelays])) const capped = capAutoSelectedRelays(result.selectableRelays, selectedWithCache) + autoSelectedRelayUrlsRef.current = capped setSelectedRelayUrls(capped) setDescription(describeRelaySelection(capped)) if (selectableRelaysChanged && hasManualSelection) { @@ -246,16 +247,8 @@ export default function PostRelaySelector({ // Update parent component with selected relays useEffect(() => { - // An event is "protected" if we have selected relays that aren't the default user write relays - const defaultUserWriteRelays = [...(relayList?.httpWrite ?? []), ...(relayList?.write || [])] - const normW = (u: string) => normalizeRelayUrlByScheme(u) || u - const defaultNorm = new Set(defaultUserWriteRelays.map(normW)) - const isProtectedEvent = - selectedRelayUrls.length > 0 && - !selectedRelayUrls.every((url) => defaultNorm.has(normW(url))) - setIsProtectedEvent(isProtectedEvent) setAdditionalRelayUrls(selectedRelayUrls) - }, [selectedRelayUrls, relayList, setIsProtectedEvent, setAdditionalRelayUrls]) + }, [selectedRelayUrls, setAdditionalRelayUrls]) const handleRelayCheckedChange = useCallback((checked: boolean, url: string) => { setHasManualSelection(true) diff --git a/src/lib/discussion-thread-composer.ts b/src/lib/discussion-thread-composer.ts index 0abaaab9..7f0852a0 100644 --- a/src/lib/discussion-thread-composer.ts +++ b/src/lib/discussion-thread-composer.ts @@ -146,7 +146,7 @@ export function collectDiscussionThreadTags(params: { const { processedContent, topicForTags, title, dynamicTopics, isReadingGroup, author, subject, isNsfw } = params const images = extractImagesFromContent(processedContent) const hashtags = extractHashtagsFromContent(processedContent) - const tags: string[][] = [['title', title.trim()], ['-']] + const tags: string[][] = [['title', title.trim()]] if (topicForTags !== 'all' && topicForTags !== 'general' && topicForTags !== 'groups') { const selectedDynamicTopic = dynamicTopics?.allTopics.find((dt) => dt.id === topicForTags) diff --git a/src/lib/draft-event.ts b/src/lib/draft-event.ts index 5e619b6f..f33b7d6e 100644 --- a/src/lib/draft-event.ts +++ b/src/lib/draft-event.ts @@ -226,7 +226,6 @@ export async function createShortTextNoteDraftEvent( options: { parentEvent?: Event addClientTag?: boolean - protectedEvent?: boolean isNsfw?: boolean addExpirationTag?: boolean expirationMonths?: number @@ -271,10 +270,6 @@ export async function createShortTextNoteDraftEvent( tags.push(buildNsfwTag()) } - if (options.protectedEvent) { - tags.push(buildProtectedTag()) - } - if (options.addExpirationTag && options.expirationMonths) { tags.push(buildExpirationTag(options.expirationMonths)) } @@ -309,7 +304,6 @@ export async function createCommentDraftEvent( mentions: string[], options: { addClientTag?: boolean - protectedEvent?: boolean isNsfw?: boolean addExpirationTag?: boolean expirationMonths?: number @@ -389,10 +383,6 @@ export async function createCommentDraftEvent( tags.push(buildNsfwTag()) } - if (options.protectedEvent) { - tags.push(buildProtectedTag()) - } - if (options.addExpirationTag && options.expirationMonths) { tags.push(buildExpirationTag(options.expirationMonths)) } @@ -1620,10 +1610,6 @@ function buildNsfwTag() { return ['content-warning', 'NSFW'] } -function buildProtectedTag() { - return ['-'] -} - function buildExpirationTag(months: number): string[] { const expirationTime = dayjs().add(months, 'month').unix() return ['expiration', expirationTime.toString()] @@ -1847,7 +1833,6 @@ export async function createVoiceCommentDraftEvent( mentions: string[], options: { addClientTag?: boolean - protectedEvent?: boolean isNsfw?: boolean addExpirationTag?: boolean expirationMonths?: number @@ -1926,10 +1911,6 @@ export async function createVoiceCommentDraftEvent( tags.push(buildNsfwTag()) } - if (options.protectedEvent) { - tags.push(buildProtectedTag()) - } - if (options.addExpirationTag && options.expirationMonths) { tags.push(buildExpirationTag(options.expirationMonths)) } diff --git a/src/services/relay-selection.service.ts b/src/services/relay-selection.service.ts index 9f93030c..db509d35 100644 --- a/src/services/relay-selection.service.ts +++ b/src/services/relay-selection.service.ts @@ -18,6 +18,7 @@ import logger from '@/lib/logger' import indexedDb from '@/services/indexed-db.service' import { getHttpRelayListFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import { stripLocalNetworkRelaysFromRelayList } from '@/lib/relay-list-sanitize' +import { isProtectedEvent } from '@/lib/event' import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority' import nip66Service from '@/services/nip66.service' @@ -71,12 +72,14 @@ class RelaySelectionService { return normalizeRelayUrlByScheme(url) || url.trim() } - /** Kind 10002 + 10243 write/both outboxes for the logged-in user. */ + /** Kind 10432 + 10243 + 10002 write outboxes (already merged in {@link RelaySelectionContext.userWriteRelays}). */ private userWriteOutboxRelays(context: RelaySelectionContext): string[] { - return dedupeNormalizeRelayUrlsOrdered([ - ...(context.userHttpWriteRelays ?? []), - ...context.userWriteRelays - ]) + return dedupeNormalizeRelayUrlsOrdered(context.userWriteRelays) + } + + /** True when the discussion thread (kind 11 parent) uses the `-` protected tag. */ + private discussionContextIsProtected(parentEvent: Event): boolean { + return isProtectedEvent(parentEvent) } private filterLocalRelaysFromOthers(relays: string[], isOwnRelays: boolean = false): string[] { @@ -669,14 +672,16 @@ class RelaySelectionService { /** * Get relays for discussion replies (kind 11 or kind 1111) - * Includes: relay hints from kind 11, wss://thecitadel.nostr1.com, user's outboxes, and local relays + * Includes: relay hints from kind 11, wss://thecitadel.nostr1.com, user's outboxes, and local relays. + * Protected threads (`-` tag on kind 11): hints + citadel + cache only — general outboxes reject protected events. */ private async getDiscussionReplyRelays(context: RelaySelectionContext): Promise { const { parentEvent, userPubkey, blockedRelays } = context if (!parentEvent) return [] const relayUrls = new Set() - const userOutboxes = this.userWriteOutboxRelays(context) + const threadIsProtected = this.discussionContextIsProtected(parentEvent) + const userOutboxes = threadIsProtected ? [] : this.userWriteOutboxRelays(context) // Step 1: Get relay hints from the kind 11 event let discussionEventId: string | null = null @@ -710,25 +715,26 @@ class RelaySelectionService { relayUrls.add(thecitadelUrl) } - // Step 3: Add user's outboxes (NIP-65 + HTTP index write relays) - if (userOutboxes.length > 0) { - userOutboxes.forEach((url) => { - const normalized = this.normRelay(url) - if (normalized) relayUrls.add(normalized) - }) - } else if (userPubkey) { - // Fetch user's relay list if not provided - try { - const relayList = await this.getCachedRelayList(userPubkey) - if (relayList) { - const outboxes = await collectViewerWriteOutboxUrls(userPubkey, relayList) - outboxes.forEach((url) => { - const normalized = this.normRelay(url) - if (normalized) relayUrls.add(normalized) - }) + // Step 3: User outboxes (skip for protected threads — most public relays reject `-` events) + if (!threadIsProtected) { + if (userOutboxes.length > 0) { + userOutboxes.forEach((url) => { + const normalized = this.normRelay(url) + if (normalized) relayUrls.add(normalized) + }) + } else if (userPubkey) { + try { + const relayList = await this.getCachedRelayList(userPubkey) + if (relayList) { + const outboxes = await collectViewerWriteOutboxUrls(userPubkey, relayList) + outboxes.forEach((url) => { + const normalized = this.normRelay(url) + if (normalized) relayUrls.add(normalized) + }) + } + } catch (error) { + logger.warn('Failed to fetch user relay list for discussion reply', { error, userPubkey }) } - } catch (error) { - logger.warn('Failed to fetch user relay list for discussion reply', { error, userPubkey }) } }