You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
179 lines
5.1 KiB
179 lines
5.1 KiB
import React from 'react' |
|
import { Check, X } from 'lucide-react' |
|
import { simplifyUrl } from '@/lib/url' |
|
|
|
/** |
|
* Format relay error messages to be more user-friendly |
|
*/ |
|
function formatRelayError(error: string): string { |
|
const lowerError = error.toLowerCase() |
|
|
|
// Handle confusing relay error messages |
|
if (lowerError.includes('blocked') && lowerError.includes('event marked as protected')) { |
|
return 'Relay rejected this content (may be due to content policy)' |
|
} |
|
|
|
if (lowerError.includes('blocked')) { |
|
return 'Relay blocked this content' |
|
} |
|
|
|
if (lowerError.includes('rate limit') || lowerError.includes('rate-limit')) { |
|
return 'Rate limited - please wait before trying again' |
|
} |
|
|
|
if (lowerError.includes('auth') && lowerError.includes('required')) { |
|
return 'Authentication required' |
|
} |
|
|
|
if (lowerError.includes('writes disabled') || lowerError.includes('write disabled')) { |
|
return 'Relay has temporarily disabled writes' |
|
} |
|
|
|
if (lowerError.includes('invalid key')) { |
|
return 'Authentication failed - invalid key' |
|
} |
|
|
|
if (lowerError.includes('timeout')) { |
|
return 'Request timed out' |
|
} |
|
|
|
if (lowerError.includes('connection') && lowerError.includes('refused')) { |
|
return 'Connection refused by relay' |
|
} |
|
|
|
// Return original error if no specific formatting applies |
|
return error |
|
} |
|
|
|
/** |
|
* Render text with URLs as clickable hyperlinks |
|
*/ |
|
function renderTextWithLinks(text: string): React.ReactNode { |
|
// URL regex pattern - matches http://, https://, ws://, wss:// URLs |
|
const urlRegex = /(https?:\/\/[^\s]+|wss?:\/\/[^\s]+)/gi |
|
const parts: React.ReactNode[] = [] |
|
let lastIndex = 0 |
|
let match: RegExpExecArray | null |
|
|
|
while ((match = urlRegex.exec(text)) !== null) { |
|
// Add text before the URL |
|
if (match.index > lastIndex) { |
|
parts.push(text.substring(lastIndex, match.index)) |
|
} |
|
|
|
// Add the URL as a link |
|
const url = match[0] |
|
parts.push( |
|
<a |
|
key={match.index} |
|
href={url} |
|
target="_blank" |
|
rel="noopener noreferrer" |
|
className="text-blue-600 dark:text-blue-400 hover:underline break-all" |
|
onClick={(e) => e.stopPropagation()} |
|
> |
|
{url} |
|
</a> |
|
) |
|
|
|
lastIndex = match.index + match[0].length |
|
} |
|
|
|
// Add remaining text |
|
if (lastIndex < text.length) { |
|
parts.push(text.substring(lastIndex)) |
|
} |
|
|
|
return parts.length > 0 ? <>{parts}</> : text |
|
} |
|
|
|
interface RelayStatus { |
|
url: string |
|
success: boolean |
|
error?: string |
|
message?: string |
|
authAttempted?: boolean |
|
} |
|
|
|
interface RelayStatusDisplayProps { |
|
relayStatuses: RelayStatus[] |
|
successCount: number |
|
totalCount: number |
|
className?: string |
|
/** |
|
* When `false`, hides the aggregate line. When a node, renders it instead of the default |
|
* “Published to …” copy (e.g. timeline REQ outcomes). |
|
*/ |
|
aggregateSummary?: React.ReactNode | false |
|
} |
|
|
|
export default function RelayStatusDisplay({ |
|
relayStatuses, |
|
successCount, |
|
totalCount, |
|
className = '', |
|
aggregateSummary |
|
}: RelayStatusDisplayProps) { |
|
if (relayStatuses.length === 0) { |
|
return null |
|
} |
|
|
|
const defaultSummary = ( |
|
<div className="text-sm font-medium text-gray-700 dark:text-gray-300"> |
|
Published to {successCount} of {totalCount} relays |
|
</div> |
|
) |
|
|
|
return ( |
|
<div className={`space-y-2 ${className}`}> |
|
{aggregateSummary === false |
|
? null |
|
: aggregateSummary !== undefined |
|
? aggregateSummary |
|
: defaultSummary} |
|
|
|
<div className="space-y-1 max-w-full"> |
|
{relayStatuses.map((status, index) => ( |
|
<div |
|
key={index} |
|
className="flex items-start gap-2 text-sm min-w-0" |
|
> |
|
<div className="flex-shrink-0 mt-0.5"> |
|
{status.success ? ( |
|
<Check className="h-4 w-4 text-green-500" /> |
|
) : ( |
|
<X className="h-4 w-4 text-red-500" /> |
|
)} |
|
</div> |
|
|
|
<div className="flex-1 min-w-0 overflow-hidden"> |
|
<div className="flex flex-col gap-1"> |
|
<div className="flex items-center gap-2 min-w-0"> |
|
<span className="font-mono text-xs break-all"> |
|
{simplifyUrl(status.url)} |
|
</span> |
|
{status.authAttempted && !status.success && ( |
|
<span className="text-xs text-red-600 dark:text-red-400 flex-shrink-0"> |
|
(auth failed) |
|
</span> |
|
)} |
|
</div> |
|
|
|
{!status.success && status.error && ( |
|
<div className="text-xs text-red-600 dark:text-red-400 break-words"> |
|
{renderTextWithLinks(formatRelayError(status.error))} |
|
</div> |
|
)} |
|
{status.success && status.message && ( |
|
<div className="text-xs text-green-600 dark:text-green-400 break-words"> |
|
{renderTextWithLinks(status.message)} |
|
</div> |
|
)} |
|
</div> |
|
</div> |
|
</div> |
|
))} |
|
</div> |
|
</div> |
|
) |
|
}
|
|
|