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 })
}
}