Browse Source

fix publishing

imwald
Silberengel 5 months ago
parent
commit
401219ef7d
  1. 38
      src/components/PostEditor/PostContent.tsx
  2. 18
      src/lib/draft-event.ts
  3. 3
      src/lib/event-metadata.ts
  4. 71
      src/lib/nostr-address.ts
  5. 7
      src/lib/publishing-feedback.tsx
  6. 16
      src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
  7. 33
      src/providers/NostrProvider/index.tsx
  8. 171
      src/services/client.service.ts
  9. 2
      src/services/local-storage.service.ts

38
src/components/PostEditor/PostContent.tsx

@ -19,7 +19,7 @@ import { ImageUp, ListTodo, LoaderCircle, MessageCircle, Settings, Smile, X } fr @@ -19,7 +19,7 @@ import { ImageUp, ListTodo, LoaderCircle, MessageCircle, Settings, Smile, X } fr
import { Event, kinds } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback'
import { showPublishingFeedback, showSimplePublishSuccess, showPublishingError } from '@/lib/publishing-feedback'
import EmojiPickerDialog from '../EmojiPickerDialog'
import Mentions, { extractMentions } from './Mentions'
import PollEditor from './PollEditor'
@ -261,7 +261,41 @@ export default function PostContent({ @@ -261,7 +261,41 @@ export default function PostContent({
close()
} catch (error) {
console.error('Publishing error:', error)
// Publishing errors are handled via relay status feedback
console.error('Error details:', {
name: error instanceof Error ? error.name : 'Unknown',
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
})
// Check if we have relay statuses to display (even if publishing failed)
if (error instanceof AggregateError && (error as any).relayStatuses) {
const relayStatuses = (error as any).relayStatuses
const successCount = relayStatuses.filter((s: any) => s.success).length
const totalCount = relayStatuses.length
// Show proper relay status feedback
showPublishingFeedback({
success: successCount > 0,
relayStatuses,
successCount,
totalCount
}, {
message: successCount > 0 ?
(parentEvent ? t('Reply published to some relays') : t('Post published to some relays')) :
(parentEvent ? t('Failed to publish reply') : t('Failed to publish post')),
duration: 6000
})
} else {
// Use standard publishing error feedback for cases without relay statuses
if (error instanceof AggregateError) {
const errorMessages = error.errors.map((err: any) => err.message).join('; ')
showPublishingError(`Failed to publish to relays: ${errorMessages}`)
} else if (error instanceof Error) {
showPublishingError(error.message)
} else {
showPublishingError('Failed to publish')
}
}
} finally {
setPosting(false)
}

18
src/lib/draft-event.ts

@ -2,6 +2,7 @@ import { ApplicationDataKey, EMBEDDED_EVENT_REGEX, ExtendedKind, POLL_TYPE } fro @@ -2,6 +2,7 @@ import { ApplicationDataKey, EMBEDDED_EVENT_REGEX, ExtendedKind, POLL_TYPE } fro
import client from '@/services/client.service'
import customEmojiService from '@/services/custom-emoji.service'
import mediaUpload from '@/services/media-upload.service'
import { prefixNostrAddresses } from '@/lib/nostr-address'
import {
TDraftEvent,
TEmoji,
@ -112,7 +113,9 @@ export async function createShortTextNoteDraftEvent( @@ -112,7 +113,9 @@ export async function createShortTextNoteDraftEvent(
isNsfw?: boolean
} = {}
): Promise<TDraftEvent> {
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(content)
// Process content to prefix nostr addresses before other transformations
const contentWithPrefixedAddresses = prefixNostrAddresses(content)
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(contentWithPrefixedAddresses)
const { quoteEventHexIds, quoteReplaceableCoordinates, rootETag, parentETag } =
await extractRelatedEventIds(transformedEmojisContent, options.parentEvent)
const hashtags = extractHashtags(transformedEmojisContent)
@ -175,6 +178,7 @@ export function createRelaySetDraftEvent(relaySet: Omit<TRelaySet, 'aTag'>): TDr @@ -175,6 +178,7 @@ export function createRelaySetDraftEvent(relaySet: Omit<TRelaySet, 'aTag'>): TDr
}
}
export async function createCommentDraftEvent(
content: string,
parentEvent: Event,
@ -185,7 +189,9 @@ export async function createCommentDraftEvent( @@ -185,7 +189,9 @@ export async function createCommentDraftEvent(
isNsfw?: boolean
} = {}
): Promise<TDraftEvent> {
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(content)
// Process content to prefix nostr addresses before other transformations
const contentWithPrefixedAddresses = prefixNostrAddresses(content)
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(contentWithPrefixedAddresses)
const {
quoteEventHexIds,
quoteReplaceableCoordinates,
@ -265,7 +271,9 @@ export async function createPublicMessageReplyDraftEvent( @@ -265,7 +271,9 @@ export async function createPublicMessageReplyDraftEvent(
isNsfw?: boolean
} = {}
): Promise<TDraftEvent> {
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(content)
// Process content to prefix nostr addresses before other transformations
const contentWithPrefixedAddresses = prefixNostrAddresses(content)
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(contentWithPrefixedAddresses)
const {
quoteEventHexIds,
quoteReplaceableCoordinates
@ -332,7 +340,9 @@ export async function createPublicMessageDraftEvent( @@ -332,7 +340,9 @@ export async function createPublicMessageDraftEvent(
isNsfw?: boolean
} = {}
): Promise<TDraftEvent> {
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(content)
// Process content to prefix nostr addresses before other transformations
const contentWithPrefixedAddresses = prefixNostrAddresses(content)
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(contentWithPrefixedAddresses)
const hashtags = extractHashtags(transformedEmojisContent)
const tags = emojiTags

3
src/lib/event-metadata.ts

@ -158,10 +158,7 @@ export function getZapInfoFromEvent(receiptEvent: Event) { @@ -158,10 +158,7 @@ export function getZapInfoFromEvent(receiptEvent: Event) {
if (amountTag && amountTag[1]) {
const millisats = parseInt(amountTag[1])
amount = millisats / 1000 // Convert millisats to sats
console.log(`📝 Parsed amount from description tag: ${amountTag[1]} millisats = ${amount} sats`)
}
} else if (amount > 0) {
console.log(`📝 Parsed amount from invoice: ${amount} sats`)
}
} catch {
// ignore

71
src/lib/nostr-address.ts

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
/**
* Utility functions for handling nostr addresses
*/
/**
* Regex pattern for matching nostr addresses that don't already have a prefix
* Matches npub, nprofile, note, nevent, naddr, nrelay patterns
*/
const NOSTR_ADDRESS_REGEX = /\b(npub|nprofile|note|nevent|naddr|nrelay)1[a-z0-9]+/gi
/**
* Prefixes nostr addresses with "nostr:" if they don't already have a prefix
* @param content - The content to process
* @returns The content with nostr addresses properly prefixed
*/
export function prefixNostrAddresses(content: string): string {
return content.replace(NOSTR_ADDRESS_REGEX, (match) => {
// Check if it already has a prefix (nostr: or other protocol)
const beforeMatch = content.substring(0, content.indexOf(match))
const lastSpace = beforeMatch.lastIndexOf(' ')
const lastNewline = beforeMatch.lastIndexOf('\n')
const lastDelimiter = Math.max(lastSpace, lastNewline)
if (lastDelimiter >= 0) {
const prefix = content.substring(lastDelimiter + 1, content.indexOf(match))
// If it already has nostr: prefix, don't add another
if (prefix.includes('nostr:')) {
return match
}
}
// Add nostr: prefix
return `nostr:${match}`
})
}
/**
* Checks if a string contains nostr addresses that need prefixing
* @param content - The content to check
* @returns True if the content contains unprefixed nostr addresses
*/
export function containsUnprefixedNostrAddresses(content: string): boolean {
return NOSTR_ADDRESS_REGEX.test(content)
}
/**
* Extracts all nostr addresses from content (both prefixed and unprefixed)
* @param content - The content to extract addresses from
* @returns Array of nostr addresses found
*/
export function extractNostrAddresses(content: string): string[] {
// Reset regex state
NOSTR_ADDRESS_REGEX.lastIndex = 0
const addresses: string[] = []
let match
while ((match = NOSTR_ADDRESS_REGEX.exec(content)) !== null) {
addresses.push(match[0])
}
// Also check for already prefixed addresses
const prefixedRegex = /\bnostr:(npub|nprofile|note|nevent|naddr|nrelay)1[a-z0-9]+/gi
prefixedRegex.lastIndex = 0
while ((match = prefixedRegex.exec(content)) !== null) {
addresses.push(match[0])
}
return addresses
}

7
src/lib/publishing-feedback.tsx

@ -39,10 +39,13 @@ export function showPublishingFeedback( @@ -39,10 +39,13 @@ export function showPublishingFeedback(
}
// Show toast with custom relay status display
toast.success(
const isSuccess = successCount > 0
const toastFunction = isSuccess ? toast.success : toast.error
toastFunction(
<div className="w-full">
<div className="flex items-center gap-2 mb-3">
<CheckCircle2 className="w-5 h-5 text-green-500" />
<CheckCircle2 className={`w-5 h-5 ${isSuccess ? 'text-green-500' : 'text-red-500'}`} />
<div className="font-semibold">{message}</div>
</div>
<div className="text-xs text-muted-foreground mb-2">

16
src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx

@ -12,6 +12,8 @@ import { useState } from 'react' @@ -12,6 +12,8 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNostr } from '@/providers/NostrProvider'
import { TDraftEvent } from '@/types'
import { prefixNostrAddresses } from '@/lib/nostr-address'
import { showPublishingError } from '@/lib/publishing-feedback'
import dayjs from 'dayjs'
// Utility functions for thread creation
@ -32,6 +34,7 @@ function buildClientTag(): string[] { @@ -32,6 +34,7 @@ function buildClientTag(): string[] {
return ['client', 'jumble']
}
interface CreateThreadDialogProps {
topic: string
availableRelays: string[]
@ -125,7 +128,7 @@ export default function CreateThreadDialog({ @@ -125,7 +128,7 @@ export default function CreateThreadDialog({
e.preventDefault()
if (!pubkey) {
alert(t('You must be logged in to create a thread'))
showPublishingError(t('You must be logged in to create a thread'))
return
}
@ -136,8 +139,11 @@ export default function CreateThreadDialog({ @@ -136,8 +139,11 @@ export default function CreateThreadDialog({
setIsSubmitting(true)
try {
// Extract images from content
const images = extractImagesFromContent(content.trim())
// Process content to prefix nostr addresses
const processedContent = prefixNostrAddresses(content.trim())
// Extract images from processed content
const images = extractImagesFromContent(processedContent)
// Build tags array
const tags = [
@ -171,7 +177,7 @@ export default function CreateThreadDialog({ @@ -171,7 +177,7 @@ export default function CreateThreadDialog({
// Create the thread event (kind 11)
const threadEvent: TDraftEvent = {
kind: 11,
content: content.trim(),
content: processedContent,
tags,
created_at: dayjs().unix()
}
@ -219,7 +225,7 @@ export default function CreateThreadDialog({ @@ -219,7 +225,7 @@ export default function CreateThreadDialog({
errorMessage = t('Failed to publish to some relays. Please try again or use different relays.')
}
alert(errorMessage)
showPublishingError(errorMessage)
} finally {
setIsSubmitting(false)
}

33
src/providers/NostrProvider/index.tsx

@ -631,18 +631,29 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -631,18 +631,29 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const relays = await client.determineTargetRelays(event, options)
const publishResult = await client.publishEvent(relays, event)
console.log('Publish result:', publishResult)
// Store relay status for display
if (publishResult.relayStatuses.length > 0) {
// We'll pass this to the UI components that need it
(event as any).relayStatuses = publishResult.relayStatuses
console.log('Attached relay statuses to event:', (event as any).relayStatuses)
try {
const publishResult = await client.publishEvent(relays, event)
console.log('Publish result:', publishResult)
// Store relay status for display
if (publishResult.relayStatuses.length > 0) {
// We'll pass this to the UI components that need it
(event as any).relayStatuses = publishResult.relayStatuses
console.log('Attached relay statuses to event:', (event as any).relayStatuses)
}
return event
} catch (error) {
// Even if publishing fails, try to extract relay statuses from the error
if (error instanceof AggregateError && (error as any).relayStatuses) {
(event as any).relayStatuses = (error as any).relayStatuses
console.log('Attached relay statuses from error:', (event as any).relayStatuses)
}
// Re-throw the error so the UI can handle it appropriately
throw error
}
return event
}
const attemptDelete = async (targetEvent: Event) => {

171
src/services/client.service.ts

@ -90,67 +90,23 @@ class ClientService extends EventTarget { @@ -90,67 +90,23 @@ class ClientService extends EventTarget {
{ specifiedRelayUrls, additionalRelayUrls }: TPublishOptions = {}
) {
let relays: string[]
if (specifiedRelayUrls?.length) {
// Check if this is a discussion thread or reply to a discussion
const isDiscussionRelated = event.kind === ExtendedKind.DISCUSSION ||
event.tags.some(tag => tag[0] === 'k' && tag[1] === String(ExtendedKind.DISCUSSION))
// Special handling for discussion-related events: try specified relay first, then fallback
if (specifiedRelayUrls?.length && (event.kind === ExtendedKind.DISCUSSION || event.kind === ExtendedKind.COMMENT)) {
// For discussion replies, try ONLY the specified relay first
// The fallback will be handled in the publishing logic if this fails
relays = specifiedRelayUrls
return relays
} else if (specifiedRelayUrls?.length) {
// For non-discussion events, use specified relays as-is
relays = specifiedRelayUrls
} else {
const _additionalRelayUrls: string[] = additionalRelayUrls ?? []
// Check if this is a discussion thread or reply to a discussion
const isDiscussionRelated = event.kind === ExtendedKind.DISCUSSION ||
event.tags.some(tag => tag[0] === 'k' && tag[1] === String(ExtendedKind.DISCUSSION))
// Special handling for kind 11 (DISCUSSION) and kind 1111 (COMMENT/NIP-22)
// These should only be published to the relay where the original event was found
// or to the relay hint specified in the event tags
if (event.kind === ExtendedKind.DISCUSSION || event.kind === ExtendedKind.COMMENT) {
let rootEventId: string | undefined
let relayHint: string | undefined
if (event.kind === ExtendedKind.COMMENT) {
// Kind 1111 (NIP-22 Comment): look for 'E' tag which points to the root event
// Format: ["E", "<id>", "<relay hint>", "<root pubkey>"]
const ETag = event.tags.find(tag => tag[0] === 'E')
if (ETag) {
rootEventId = ETag[1]
relayHint = ETag[2] // Relay hint is the 3rd element
}
// If no 'E' tag, check lowercase 'e' tag for parent event
// This handles cases where we're replying to a reply
if (!rootEventId) {
const eTag = event.tags.find(tag => tag[0] === 'e')
if (eTag) {
rootEventId = eTag[1]
relayHint = eTag[2]
}
}
} else if (event.kind === ExtendedKind.DISCUSSION) {
// Kind 11 (DISCUSSION): this is the root event itself
// For new root events, we can use the specified relays or fall through to normal handling
// But for replies TO kind 11, we should have caught them as kind 1111 above
rootEventId = event.id
}
// Priority 1: Use relay hint from the tag if present and valid
if (relayHint && isWebsocketUrl(relayHint)) {
const normalizedRelayHint = normalizeUrl(relayHint)
if (normalizedRelayHint) {
relays = [normalizedRelayHint]
return relays
}
}
// Priority 2: Get relay where the root event was found
if (rootEventId) {
const originalEventRelays = this.getEventHints(rootEventId)
if (originalEventRelays.length > 0) {
// Only publish to the relay(s) where the original event was found
relays = originalEventRelays.slice(0, 1) // Use only the first relay for insular discussions
return relays
}
}
}
// Publish to mentioned users' inboxes for all events EXCEPT discussions
if (!isDiscussionRelated) {
const mentions: string[] = []
@ -208,6 +164,78 @@ class ClientService extends EventTarget { @@ -208,6 +164,78 @@ class ClientService extends EventTarget {
}>
successCount: number
totalCount: number
}> {
// Special handling for discussion events: try relay hint first, then fallback
if ((event.kind === ExtendedKind.DISCUSSION || event.kind === ExtendedKind.COMMENT) && relayUrls.length === 1) {
try {
// Try publishing to the relay hint first
const result = await this._publishToRelays(relayUrls, event)
// If successful, return the result
if (result.success) {
return result
}
// If failed, try fallback relays
const userRelays = this.pubkey ? await this.fetchRelayList(this.pubkey) : { write: [], read: [] }
const fallbackRelays = userRelays.write.length > 0 ? userRelays.write.slice(0, 3) : FAST_WRITE_RELAY_URLS
console.log('Relay hint failed, trying fallback relays:', fallbackRelays)
const fallbackResult = await this._publishToRelays(fallbackRelays, event)
// Combine relay statuses from both attempts
const combinedRelayStatuses = [...result.relayStatuses, ...fallbackResult.relayStatuses]
const combinedSuccessCount = combinedRelayStatuses.filter(s => s.success).length
return {
success: combinedSuccessCount > 0,
relayStatuses: combinedRelayStatuses,
successCount: combinedSuccessCount,
totalCount: combinedRelayStatuses.length
}
} catch (error) {
// If relay hint throws an error, try fallback relays
console.log('Relay hint threw error, trying fallback relays:', error)
// Extract relay statuses from the error if available
let hintRelayStatuses: any[] = []
if (error instanceof AggregateError && (error as any).relayStatuses) {
hintRelayStatuses = (error as any).relayStatuses
}
const userRelays = this.pubkey ? await this.fetchRelayList(this.pubkey) : { write: [], read: [] }
const fallbackRelays = userRelays.write.length > 0 ? userRelays.write.slice(0, 3) : FAST_WRITE_RELAY_URLS
console.log('Trying fallback relays:', fallbackRelays)
const fallbackResult = await this._publishToRelays(fallbackRelays, event)
// Combine relay statuses from both attempts
const combinedRelayStatuses = [...hintRelayStatuses, ...fallbackResult.relayStatuses]
const combinedSuccessCount = combinedRelayStatuses.filter(s => s.success).length
return {
success: combinedSuccessCount > 0,
relayStatuses: combinedRelayStatuses,
successCount: combinedSuccessCount,
totalCount: combinedRelayStatuses.length
}
}
}
// For non-discussion events, use normal publishing
return await this._publishToRelays(relayUrls, event)
}
private async _publishToRelays(relayUrls: string[], event: NEvent): Promise<{
success: boolean
relayStatuses: Array<{
url: string
success: boolean
error?: string
authAttempted?: boolean
}>
successCount: number
totalCount: number
}> {
const uniqueRelayUrls = this.optimizeRelaySelection(Array.from(new Set(relayUrls)))
@ -258,21 +286,22 @@ class ClientService extends EventTarget { @@ -258,21 +286,22 @@ class ClientService extends EventTarget {
})
} else {
resolved = true
reject(
new AggregateError(
errors.map(
({ url, error }) => {
let errorMsg = 'Unknown error'
if (error instanceof Error) {
errorMsg = error.message || 'Empty error message'
} else if (error !== null && error !== undefined) {
errorMsg = String(error)
}
return new Error(`Failed to publish to ${url}: ${errorMsg}`)
const aggregateError = new AggregateError(
errors.map(
({ url, error }) => {
let errorMsg = 'Unknown error'
if (error instanceof Error) {
errorMsg = error.message || 'Empty error message'
} else if (error !== null && error !== undefined) {
errorMsg = String(error)
}
)
return new Error(`Failed to publish to ${url}: ${errorMsg}`)
}
)
)
// Attach relay statuses to the error so they can be displayed
;(aggregateError as any).relayStatuses = relayStatuses
reject(aggregateError)
}
}
}
@ -1216,17 +1245,13 @@ class ClientService extends EventTarget { @@ -1216,17 +1245,13 @@ class ClientService extends EventTarget {
// Normalize relay URLs (remove trailing slashes for consistency)
const normalizedUrls = relayUrls.map(url => url.endsWith('/') ? url.slice(0, -1) : url)
console.log(`Trying to fetch from ${normalizedUrls.length} relays:`, normalizedUrls)
const events = await this.query(normalizedUrls, filter)
console.log(`Found ${events.length} events from relays`)
if (events.length === 0) {
console.log('No events found, continuing to next tier')
return undefined
}
const result = events.sort((a, b) => b.created_at - a.created_at)[0]
console.log('Found event:', result.id)
return result
} catch (error) {
console.error('Error in tryHarderToFetchEvent:', error)

2
src/services/local-storage.service.ts

@ -34,7 +34,7 @@ class LocalStorageService { @@ -34,7 +34,7 @@ class LocalStorageService {
private defaultZapSats: number = 21
private defaultZapComment: string = 'Zap!'
private quickZap: boolean = false
private zapReplyThreshold: number = 210
private zapReplyThreshold: number = 2100
private accountFeedInfoMap: Record<string, TFeedInfo | undefined> = {}
private mediaUploadService: string = DEFAULT_NIP_96_SERVICE
private autoplay: boolean = true

Loading…
Cancel
Save