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.
 
 
 
 
 

417 lines
12 KiB

<script lang="ts">
import { nostrClient } from '../../services/nostr/nostr-client.js';
import { Relay } from 'nostr-tools';
import { onMount } from 'svelte';
interface Props {
relayUrl: string;
}
let { relayUrl }: Props = $props();
interface RelayMetadata {
name?: string;
description?: string;
pubkey?: string;
contact?: string;
supported_nips?: number[];
software?: string;
version?: string;
limitation?: {
max_message_length?: number;
max_subscriptions?: number;
max_filters?: number;
max_limit?: number;
max_subid_length?: number;
min_prefix?: number;
max_event_tags?: number;
max_content_length?: number;
min_pow_difficulty?: number;
auth_required?: boolean;
payment_required?: boolean;
restricted_writes?: boolean;
};
}
let metadata = $state<RelayMetadata | null>(null);
let loading = $state(true);
let error = $state<string | null>(null);
let connectionStatus = $state<'connecting' | 'connected' | 'disconnected'>('connecting');
let eventCount = $state<number | null>(null);
async function fetchRelayMetadata() {
loading = true;
error = null;
try {
// Get relay connection status from nostr-client
const relay = await nostrClient.getRelay(relayUrl);
if (relay) {
// Check connection status
const status = (relay as any).status;
if (status === 1) {
connectionStatus = 'connected';
} else if (status === 0) {
connectionStatus = 'connecting';
} else {
connectionStatus = 'disconnected';
}
// Fetch NIP-11 metadata via HTTP GET
try {
// Convert ws:// or wss:// to http:// or https://
const httpUrl = relayUrl.replace(/^wss?:\/\//, (match) => {
return match === 'wss://' ? 'https://' : 'http://';
});
const nip11Url = `${httpUrl}/.well-known/nostr.json`;
const response = await fetch(nip11Url, {
method: 'GET',
headers: {
'Accept': 'application/nostr+json'
}
});
if (response.ok) {
const info = await response.json();
if (info) {
metadata = info as RelayMetadata;
}
}
} catch (err) {
console.debug(`[RelayInfo] Could not fetch NIP-11 metadata for ${relayUrl}:`, err);
// Not all relays support NIP-11, so this is not a critical error
}
} else {
connectionStatus = 'disconnected';
error = 'Failed to connect to relay';
}
} catch (err) {
console.error(`[RelayInfo] Error fetching relay info for ${relayUrl}:`, err);
error = err instanceof Error ? err.message : 'Unknown error';
connectionStatus = 'disconnected';
} finally {
loading = false;
}
}
async function getEventCount() {
try {
// Fetch a small sample to estimate activity
const events = await nostrClient.fetchEvents(
[{ kinds: [1], limit: 1 }],
[relayUrl],
{ useCache: false, cacheResults: false, timeout: 5000 }
);
// This is just a connectivity check, not a real count
// Real event count would require a COUNT query which not all relays support
eventCount = events.length > 0 ? -1 : 0; // -1 means "has events" but we don't know the count
} catch (err) {
// Silently fail - this is just informational
eventCount = null;
}
}
onMount(async () => {
await fetchRelayMetadata();
await getEventCount();
});
function getStatusColor(status: string): string {
switch (status) {
case 'connected':
return 'text-green-600 dark:text-green-400';
case 'connecting':
return 'text-yellow-600 dark:text-yellow-400';
case 'disconnected':
return 'text-red-600 dark:text-red-400';
default:
return 'text-gray-600 dark:text-gray-400';
}
}
function getStatusIcon(status: string): string {
switch (status) {
case 'connected':
return '●';
case 'connecting':
return '○';
case 'disconnected':
return '○';
default:
return '○';
}
}
</script>
<div class="relay-info-card">
{#if loading}
<div class="relay-info-loading">
<p class="text-fog-text-light dark:text-fog-dark-text-light">Loading relay information...</p>
</div>
{:else if error}
<div class="relay-info-error">
<p class="text-fog-text dark:text-fog-dark-text">Error: {error}</p>
</div>
{:else}
<div class="relay-info-header">
<div class="relay-info-title-row">
<h3 class="relay-info-title">Relay Information</h3>
<div class="relay-status">
<span class="relay-status-icon {getStatusColor(connectionStatus)}">{getStatusIcon(connectionStatus)}</span>
<span class="relay-status-text {getStatusColor(connectionStatus)}">
{connectionStatus.charAt(0).toUpperCase() + connectionStatus.slice(1)}
</span>
</div>
</div>
<code class="relay-url">{relayUrl}</code>
</div>
{#if metadata}
<div class="relay-info-content">
{#if metadata.name}
<div class="relay-info-item">
<span class="relay-info-label">Name:</span>
<span class="relay-info-value">{metadata.name}</span>
</div>
{/if}
{#if metadata.description}
<div class="relay-info-item">
<span class="relay-info-label">Description:</span>
<span class="relay-info-value">{metadata.description}</span>
</div>
{/if}
{#if metadata.software}
<div class="relay-info-item">
<span class="relay-info-label">Software:</span>
<span class="relay-info-value">{metadata.software}{metadata.version ? ` ${metadata.version}` : ''}</span>
</div>
{/if}
{#if metadata.contact}
<div class="relay-info-item">
<span class="relay-info-label">Contact:</span>
<span class="relay-info-value">
{#if metadata.contact.startsWith('mailto:')}
<a href="{metadata.contact}" class="relay-info-link">{metadata.contact.replace('mailto:', '')}</a>
{:else if metadata.contact.startsWith('http')}
<a href="{metadata.contact}" target="_blank" rel="noopener noreferrer" class="relay-info-link">{metadata.contact}</a>
{:else}
{metadata.contact}
{/if}
</span>
</div>
{/if}
{#if metadata.pubkey}
<div class="relay-info-item">
<span class="relay-info-label">Pubkey:</span>
<code class="relay-info-value relay-pubkey">{metadata.pubkey.substring(0, 16)}...</code>
</div>
{/if}
{#if metadata.supported_nips && metadata.supported_nips.length > 0}
<div class="relay-info-item">
<span class="relay-info-label">Supported NIPs:</span>
<span class="relay-info-value">
{metadata.supported_nips.slice(0, 20).join(', ')}{metadata.supported_nips.length > 20 ? '...' : ''}
</span>
</div>
{/if}
{#if metadata.limitation}
<div class="relay-info-limitations">
<span class="relay-info-label">Limitations:</span>
<div class="relay-limitations-list">
{#if metadata.limitation.max_message_length}
<span class="relay-limitation-item">Max message: {metadata.limitation.max_message_length} bytes</span>
{/if}
{#if metadata.limitation.max_subscriptions}
<span class="relay-limitation-item">Max subscriptions: {metadata.limitation.max_subscriptions}</span>
{/if}
{#if metadata.limitation.max_filters}
<span class="relay-limitation-item">Max filters: {metadata.limitation.max_filters}</span>
{/if}
{#if metadata.limitation.max_limit}
<span class="relay-limitation-item">Max limit: {metadata.limitation.max_limit}</span>
{/if}
{#if metadata.limitation.auth_required}
<span class="relay-limitation-item">Auth required</span>
{/if}
{#if metadata.limitation.payment_required}
<span class="relay-limitation-item">Payment required</span>
{/if}
{#if metadata.limitation.restricted_writes}
<span class="relay-limitation-item">Restricted writes</span>
{/if}
</div>
</div>
{/if}
</div>
{:else}
<div class="relay-info-no-metadata">
<p class="text-fog-text-light dark:text-fog-dark-text-light">No NIP-11 metadata available</p>
</div>
{/if}
{/if}
</div>
<style>
.relay-info-card {
background: var(--fog-post, #ffffff);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
:global(.dark) .relay-info-card {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151);
}
.relay-info-header {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .relay-info-header {
border-bottom-color: var(--fog-dark-border, #374151);
}
.relay-info-title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.relay-info-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
}
:global(.dark) .relay-info-title {
color: var(--fog-dark-text, #f9fafb);
}
.relay-status {
display: flex;
align-items: center;
gap: 0.5rem;
}
.relay-status-icon {
font-size: 0.75rem;
}
.relay-status-text {
font-size: 0.875rem;
font-weight: 500;
}
.relay-url {
display: block;
font-family: monospace;
font-size: 0.875rem;
color: var(--fog-text-light, #9ca3af);
word-break: break-all;
}
:global(.dark) .relay-url {
color: var(--fog-dark-text-light, #6b7280);
}
.relay-info-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.relay-info-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.relay-info-label {
font-size: 0.875rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
}
:global(.dark) .relay-info-label {
color: var(--fog-dark-text, #f9fafb);
}
.relay-info-value {
font-size: 0.875rem;
color: var(--fog-text-light, #6b7280);
word-break: break-word;
}
:global(.dark) .relay-info-value {
color: var(--fog-dark-text-light, #9ca3af);
}
.relay-pubkey {
font-family: monospace;
}
.relay-info-link {
color: var(--fog-accent, #64748b);
text-decoration: underline;
}
.relay-info-link:hover {
color: var(--fog-text, #1f2937);
}
:global(.dark) .relay-info-link {
color: var(--fog-dark-accent, #94a3b8);
}
:global(.dark) .relay-info-link:hover {
color: var(--fog-dark-text, #f9fafb);
}
.relay-info-limitations {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
}
.relay-limitations-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.relay-limitation-item {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
background: var(--fog-highlight, #f3f4f6);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
color: var(--fog-text, #1f2937);
}
:global(.dark) .relay-limitation-item {
background: var(--fog-dark-highlight, #374151);
border-color: var(--fog-dark-border, #475569);
color: var(--fog-dark-text, #f9fafb);
}
.relay-info-loading,
.relay-info-error,
.relay-info-no-metadata {
padding: 1rem;
text-align: center;
}
</style>