Browse Source

bug-fixes

imwald
Silberengel 2 weeks ago
parent
commit
282e1bb573
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 48
      src/components/PostEditor/PostContent.tsx
  4. 101
      src/components/PostEditor/PostRelaySelector.tsx
  5. 3
      src/constants.ts
  6. 84
      src/lib/pre-publish-relay-cap.ts
  7. 29
      src/services/relay-selection.service.ts

4
package-lock.json generated

@ -1,12 +1,12 @@ @@ -1,12 +1,12 @@
{
"name": "imwald",
"version": "22.5.0",
"version": "22.5.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "imwald",
"version": "22.5.0",
"version": "22.5.1",
"license": "MIT",
"dependencies": {
"@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
{
"name": "imwald",
"version": "22.5.0",
"version": "22.5.1",
"description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true,
"type": "module",

48
src/components/PostEditor/PostContent.tsx

@ -37,7 +37,7 @@ import { @@ -37,7 +37,7 @@ import {
applyImwaldAttributionTags,
mergeUploadImetaTagsInto
} from '@/lib/draft-event'
import { ExtendedKind } from '@/constants'
import { ExtendedKind, MAX_PUBLISH_RELAYS } from '@/constants'
import { cn, isTouchDevice } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import { useFeed } from '@/providers/FeedProvider'
@ -78,6 +78,7 @@ import { nip94PairsToImetaTag } from '@/lib/upload-nip94-imeta' @@ -78,6 +78,7 @@ import { nip94PairsToImetaTag } from '@/lib/upload-nip94-imeta'
import { getMediaKindFromFile } from '@/lib/media-kind-detection'
import { hasPrivateRelays, getPrivateRelayUrls } from '@/lib/private-relays'
import mediaUpload from '@/services/media-upload.service'
import type { TPrePublishRelayCapPreview } from '@/lib/pre-publish-relay-cap'
import { successfulPublishRelayUrls, type TRelayPublishStatus } from '@/lib/publish-relay-urls'
import client, { eventService } from '@/services/client.service'
import discussionFeedCache from '@/services/discussion-feed-cache.service'
@ -219,6 +220,12 @@ export default function PostContent({ @@ -219,6 +220,12 @@ export default function PostContent({
)
const [isProtectedEvent, setIsProtectedEvent] = useState(false)
const [additionalRelayUrls, setAdditionalRelayUrls] = useState<string[]>([])
/** When set, too many relays are checked vs the per-publish cap; publish stays disabled until unchecking. */
const [relayCapBlockInfo, setRelayCapBlockInfo] = useState<{
outboxSlotsInPublish: number
selectedContacted: number
selectedTotal: number
} | null>(null)
const [isHighlight, setIsHighlight] = useState(!!initialHighlightData)
const [highlightData, setHighlightData] = useState<HighlightData>(
initialHighlightData || {
@ -386,6 +393,22 @@ export default function PostContent({ @@ -386,6 +393,22 @@ export default function PostContent({
isNsfw
])
const handleRelayPublishCapChange = useCallback((preview: TPrePublishRelayCapPreview) => {
if (preview.blocksPublish) {
setRelayCapBlockInfo({
outboxSlotsInPublish: preview.outboxSlotsInPublish,
selectedContacted: preview.selectedContacted,
selectedTotal: preview.selectedTotal
})
} else {
setRelayCapBlockInfo(null)
}
}, [])
useEffect(() => {
if (isPoll) setRelayCapBlockInfo(null)
}, [isPoll])
const canPost = useMemo(() => {
const discussionOk =
!isDiscussionThread ||
@ -413,7 +436,8 @@ export default function PostContent({ @@ -413,7 +436,8 @@ export default function PostContent({
(!isCitationInternal || !!citationInternalCTag.trim()) &&
(!isCitationExternal || (!!citationExternalUrl.trim() && !!citationAccessedOn.trim())) &&
(!isCitationHardcopy || !!citationAccessedOn.trim()) &&
(!isCitationPrompt || (!!citationPromptLlm.trim() && !!citationAccessedOn.trim()))
(!isCitationPrompt || (!!citationPromptLlm.trim() && !!citationAccessedOn.trim())) &&
relayCapBlockInfo === null
)
return result
@ -447,7 +471,8 @@ export default function PostContent({ @@ -447,7 +471,8 @@ export default function PostContent({
threadIsReadingGroup,
threadReadingAuthor,
threadReadingSubject,
threadSelectedGroup
threadSelectedGroup,
relayCapBlockInfo
])
// Clear highlight data when initialHighlightData changes or is removed
@ -3104,12 +3129,29 @@ export default function PostContent({ @@ -3104,12 +3129,29 @@ export default function PostContent({
<PostRelaySelector
setIsProtectedEvent={setIsProtectedEvent}
setAdditionalRelayUrls={setAdditionalRelayUrls}
onRelayPublishCapChange={handleRelayPublishCapChange}
parentEvent={parentEvent}
openFrom={openFrom}
content={text}
isPublicMessage={isPublicMessage}
mentions={extractedMentions}
/>
{relayCapBlockInfo && (
<p className="mt-2 text-sm text-amber-600 dark:text-amber-500" role="alert">
{relayCapBlockInfo.outboxSlotsInPublish > 0
? t('Publish relay cap hint with outbox first', {
max: MAX_PUBLISH_RELAYS,
reservedSlots: relayCapBlockInfo.outboxSlotsInPublish,
selected: relayCapBlockInfo.selectedTotal,
selectedContacted: relayCapBlockInfo.selectedContacted
})
: t('Publish relay cap hint', {
max: MAX_PUBLISH_RELAYS,
selected: relayCapBlockInfo.selectedTotal,
selectedContacted: relayCapBlockInfo.selectedContacted
})}
</p>
)}
{isDiscussionThread && threadErrors.relay && (
<p className="mt-1 text-sm text-destructive">{threadErrors.relay}</p>
)}

101
src/components/PostEditor/PostRelaySelector.tsx

@ -1,13 +1,6 @@ @@ -1,13 +1,6 @@
import {
ExtendedKind,
isSocialKindBlockedKind,
MAX_PUBLISH_RELAYS,
READ_ONLY_RELAY_URLS,
SOCIAL_KIND_BLOCKED_RELAY_URLS
} from '@/constants'
import { ExtendedKind, isSocialKindBlockedKind, MAX_PUBLISH_RELAYS, SOCIAL_KIND_BLOCKED_RELAY_URLS } from '@/constants'
import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns'
import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority'
import { simplifyUrl, isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url'
import { simplifyUrl, isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
@ -25,15 +18,31 @@ import { Button } from '@/components/ui/button' @@ -25,15 +18,31 @@ import { Button } from '@/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
import logger from '@/lib/logger'
import { computePrePublishRelayCapPreview, type TPrePublishRelayCapPreview } from '@/lib/pre-publish-relay-cap'
/** Stable default when `mentions` is omitted — inline `= []` is a new array every render and retriggers effects. */
const NO_MENTIONS: string[] = []
/** Keep auto-selection within {@link MAX_PUBLISH_RELAYS}, preserving {@link selectableRelaysOrder} (top of list first). */
function capAutoSelectedRelays(selectableRelaysOrder: string[], selectedWithCache: string[]): string[] {
const norm = (u: string) => normalizeAnyRelayUrl(u) || u
const selectedNormSet = new Set(selectedWithCache.map(norm))
const ordered: string[] = []
for (const url of selectableRelaysOrder) {
if (selectedNormSet.has(norm(url))) ordered.push(url)
}
for (const url of selectedWithCache) {
if (!ordered.some((u) => norm(u) === norm(url))) ordered.push(url)
}
return ordered.slice(0, MAX_PUBLISH_RELAYS)
}
export default function PostRelaySelector({
parentEvent: _parentEvent,
openFrom,
setIsProtectedEvent,
setAdditionalRelayUrls,
onRelayPublishCapChange,
content: postContent = '',
isPublicMessage = false,
mentions = NO_MENTIONS
@ -42,6 +51,8 @@ export default function PostRelaySelector({ @@ -42,6 +51,8 @@ export default function PostRelaySelector({
openFrom?: string[]
setIsProtectedEvent: Dispatch<SetStateAction<boolean>>
setAdditionalRelayUrls: Dispatch<SetStateAction<string[]>>
/** 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
content?: string
isPublicMessage?: boolean
mentions?: string[]
@ -91,61 +102,29 @@ export default function PostRelaySelector({ @@ -91,61 +102,29 @@ export default function PostRelaySelector({
return false
}, [_parentEvent])
/**
* Same merge order as {@link ClientService.publishEvent}: NIP-65 write list first, then relays checked here,
* then cap at {@link MAX_PUBLISH_RELAYS}. Drives the cap hint so users see reserved prepended slots.
*/
const publishCapPreview = useMemo(() => {
const applySocialOutboxFilter =
!isPublicMessage &&
(_parentEvent == null ||
isDiscussionReply ||
(_parentEvent != null && isSocialKindBlockedKind(_parentEvent.kind)))
const wsOut = (relayList?.write ?? [])
.map((u) => normalizeUrl(u) || u)
.filter((u): u is string => !!u)
const httpOut = (relayList?.httpWrite ?? [])
.map((u) => normalizeHttpRelayUrl(u) || u)
.filter((u): u is string => !!u)
let outbox = dedupeNormalizeRelayUrlsOrdered([...httpOut, ...wsOut])
const readOnlySet = new Set(READ_ONLY_RELAY_URLS.map((u) => normalizeAnyRelayUrl(u) || u))
const socialBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
outbox = dedupeNormalizeRelayUrlsOrdered(
outbox.filter((url) => {
const n = normalizeAnyRelayUrl(url) || url
if (readOnlySet.has(n)) return false
if (applySocialOutboxFilter && socialBlockedSet.has(n)) return false
return true
})
)
const merged = dedupeNormalizeRelayUrlsOrdered([...outbox, ...selectedRelayUrls])
const capped = merged.slice(0, MAX_PUBLISH_RELAYS)
const outboxNormSet = new Set(outbox)
const outboxSlotsInPublish = capped.filter((u) => outboxNormSet.has(u)).length
const selectedNorm = selectedRelayUrls.map((u) => normalizeAnyRelayUrl(u) || u)
const selectedContacted = selectedNorm.filter((u) => capped.includes(u)).length
const showCapHint =
merged.length > MAX_PUBLISH_RELAYS ||
selectedRelayUrls.length >= MAX_PUBLISH_RELAYS ||
selectedContacted < selectedRelayUrls.length
return {
outboxSlotsInPublish,
selectedContacted,
selectedTotal: selectedRelayUrls.length,
showCapHint
}
}, [
const publishCapPreview = useMemo(
() =>
computePrePublishRelayCapPreview({
relayListWrite: relayList?.write,
relayListHttpWrite: relayList?.httpWrite,
selectedRelayUrls,
isPublicMessage,
parentEvent: _parentEvent,
isDiscussionReply
}),
[
relayList?.write,
relayList?.httpWrite,
selectedRelayUrls,
isPublicMessage,
_parentEvent,
isDiscussionReply
])
]
)
useEffect(() => {
onRelayPublishCapChange?.(publishCapPreview)
}, [publishCapPreview, onRelayPublishCapChange])
/**
* Relay selection only cares about nostr: mentions in the draft (see relay-selection.service).
@ -242,8 +221,9 @@ export default function PostRelaySelector({ @@ -242,8 +221,9 @@ export default function PostRelaySelector({
if (!hasManualSelection || selectableRelaysChanged) {
const cacheRelays = result.selectableRelays.filter(url => isLocalNetworkUrl(url))
const selectedWithCache = Array.from(new Set([...result.selectedRelays, ...cacheRelays]))
setSelectedRelayUrls(selectedWithCache)
setDescription(describeRelaySelection(selectedWithCache))
const capped = capAutoSelectedRelays(result.selectableRelays, selectedWithCache)
setSelectedRelayUrls(capped)
setDescription(describeRelaySelection(capped))
if (selectableRelaysChanged && hasManualSelection) {
setHasManualSelection(false)
}
@ -395,6 +375,7 @@ export default function PostRelaySelector({ @@ -395,6 +375,7 @@ export default function PostRelaySelector({
const capHintEl =
publishCapPreview.showCapHint &&
!publishCapPreview.blocksPublish &&
(publishCapPreview.outboxSlotsInPublish > 0 ? (
<span className="text-xs text-amber-600 dark:text-amber-500">
{t('Publish relay cap hint with outbox first', {

3
src/constants.ts

@ -317,7 +317,8 @@ export const READ_ONLY_RELAY_URLS = [ @@ -317,7 +317,8 @@ export const READ_ONLY_RELAY_URLS = [
'wss://relay.noswhere.com',
'wss://search.nos.today',
'wss://trending.nostr.wine',
'wss://relay.nip46.com'
'wss://relay.nip46.com',
'wss://filter.nostr.wine'
]
/**

84
src/lib/pre-publish-relay-cap.ts

@ -0,0 +1,84 @@ @@ -0,0 +1,84 @@
import {
isSocialKindBlockedKind,
MAX_PUBLISH_RELAYS,
READ_ONLY_RELAY_URLS,
SOCIAL_KIND_BLOCKED_RELAY_URLS
} from '@/constants'
import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority'
import { normalizeAnyRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url'
import type { NostrEvent } from 'nostr-tools'
export type TPrePublishRelayCapPreview = {
outboxSlotsInPublish: number
selectedContacted: number
selectedTotal: number
showCapHint: boolean
/** True when not every checked relay in the picker can be included in the capped publish list. */
blocksPublish: boolean
}
/**
* Pre-publish preview: mirrors merge + cap order in {@link ClientService.publishEvent}: NIP-65 write list first, then
* relays checked in the post relay picker, capped at {@link MAX_PUBLISH_RELAYS}.
*/
export function computePrePublishRelayCapPreview({
relayListWrite,
relayListHttpWrite,
selectedRelayUrls,
isPublicMessage,
parentEvent,
isDiscussionReply
}: {
relayListWrite?: string[]
relayListHttpWrite?: string[]
selectedRelayUrls: string[]
isPublicMessage: boolean
parentEvent?: NostrEvent
isDiscussionReply: boolean
}): TPrePublishRelayCapPreview {
const applySocialOutboxFilter =
!isPublicMessage &&
(parentEvent == null ||
isDiscussionReply ||
(parentEvent != null && isSocialKindBlockedKind(parentEvent.kind)))
const wsOut = (relayListWrite ?? [])
.map((u) => normalizeUrl(u) || u)
.filter((u): u is string => !!u)
const httpOut = (relayListHttpWrite ?? [])
.map((u) => normalizeHttpRelayUrl(u) || u)
.filter((u): u is string => !!u)
let outbox = dedupeNormalizeRelayUrlsOrdered([...httpOut, ...wsOut])
const readOnlySet = new Set(READ_ONLY_RELAY_URLS.map((u) => normalizeAnyRelayUrl(u) || u))
const socialBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
outbox = dedupeNormalizeRelayUrlsOrdered(
outbox.filter((url) => {
const n = normalizeAnyRelayUrl(url) || url
if (readOnlySet.has(n)) return false
if (applySocialOutboxFilter && socialBlockedSet.has(n)) return false
return true
})
)
const merged = dedupeNormalizeRelayUrlsOrdered([...outbox, ...selectedRelayUrls])
const capped = merged.slice(0, MAX_PUBLISH_RELAYS)
const outboxNormSet = new Set(outbox)
const outboxSlotsInPublish = capped.filter((u) => outboxNormSet.has(u)).length
const selectedNorm = selectedRelayUrls.map((u) => normalizeAnyRelayUrl(u) || u)
const selectedContacted = selectedNorm.filter((u) => capped.includes(u)).length
const showCapHint =
merged.length > MAX_PUBLISH_RELAYS ||
selectedRelayUrls.length >= MAX_PUBLISH_RELAYS ||
selectedContacted < selectedRelayUrls.length
const blocksPublish = selectedContacted < selectedRelayUrls.length
return {
outboxSlotsInPublish,
selectedContacted,
selectedTotal: selectedRelayUrls.length,
showCapHint,
blocksPublish
}
}

29
src/services/relay-selection.service.ts

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import { Event, kinds } from 'nostr-tools'
import { ExtendedKind, FAST_WRITE_RELAY_URLS, RANDOM_PUBLISH_RELAY_COUNT } from '@/constants'
import { ExtendedKind, FAST_WRITE_RELAY_URLS, RANDOM_PUBLISH_RELAY_COUNT, READ_ONLY_RELAY_URLS } from '@/constants'
import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns'
import client from '@/services/client.service'
import { eventService } from '@/services/client.service'
@ -175,12 +175,18 @@ class RelaySelectionService { @@ -175,12 +175,18 @@ class RelaySelectionService {
}
const deduplicatedRelays = order.map((o) => o.url)
const filtered = this.filterBlockedRelays(deduplicatedRelays, context.blockedRelays)
const filtered = this.filterReadOnlyRelays(
this.filterBlockedRelays(deduplicatedRelays, context.blockedRelays)
)
const relayTypes: Record<string, RelaySourceType> = {}
order.forEach(({ url, type }) => {
if (filtered.includes(url)) relayTypes[url] = type
})
return { relays: filtered, relayTypes, randomRelayUrls }
return {
relays: filtered,
relayTypes,
randomRelayUrls: this.filterReadOnlyRelays(randomRelayUrls)
}
}
/**
@ -429,8 +435,7 @@ class RelaySelectionService { @@ -429,8 +435,7 @@ class RelaySelectionService {
selectedRelays = Array.from(new Set(selectedRelays))
}
// Filter out blocked relays
return this.filterBlockedRelays(selectedRelays, context.blockedRelays)
return this.filterReadOnlyRelays(this.filterBlockedRelays(selectedRelays, context.blockedRelays))
}
/**
@ -799,6 +804,20 @@ class RelaySelectionService { @@ -799,6 +804,20 @@ class RelaySelectionService {
}
}
/**
* Strip relays that never accept writes ({@link READ_ONLY_RELAY_URLS}) so they do not appear in the publish picker.
* Same set as `ClientService` uses when filtering publish targets.
*/
private filterReadOnlyRelays(relays: string[]): string[] {
const readOnlySet = new Set(
READ_ONLY_RELAY_URLS.map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean)
)
return relays.filter((relay) => {
const n = normalizeAnyRelayUrl(relay) || relay
return !readOnlySet.has(n)
})
}
/**
* Filter out blocked relays from a list
*/

Loading…
Cancel
Save