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.
 
 
 
 
 

312 lines
8.1 KiB

<script lang="ts">
import Header from '../../lib/components/layout/Header.svelte';
import { nostrClient } from '../../lib/services/nostr/nostr-client.js';
import { relayManager } from '../../lib/services/nostr/relay-manager.js';
import { config } from '../../lib/services/nostr/config.js';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { sessionManager } from '../../lib/services/auth/session-manager.js';
interface RelayInfo {
url: string;
categories: string[];
connected: boolean;
}
let relays = $state<RelayInfo[]>([]);
let loading = $state(true);
function categorizeRelay(url: string): string[] {
const categories: string[] = [];
// Check all categories - a relay can belong to multiple
if (config.defaultRelays.includes(url)) {
categories.push('Default');
}
if (config.profileRelays.includes(url)) {
categories.push('Profile');
}
if (config.threadPublishRelays.includes(url)) {
categories.push('Thread Publish');
}
if (config.gifRelays.includes(url)) {
categories.push('GIF');
}
// If no categories found, mark as Other
if (categories.length === 0) {
categories.push('Other');
}
return categories;
}
async function loadRelays() {
loading = true;
// Collect all unique relays from all categories
const allRelays = new Set<string>();
// Add default relays
config.defaultRelays.forEach(r => allRelays.add(r));
// Add profile relays
config.profileRelays.forEach(r => allRelays.add(r));
// Add thread publish relays
config.threadPublishRelays.forEach(r => allRelays.add(r));
// Add gif relays
config.gifRelays.forEach(r => allRelays.add(r));
// Get connection status from nostrClient
const connectedRelays = nostrClient.getConnectedRelays();
const relayList: RelayInfo[] = Array.from(allRelays).map(url => ({
url,
categories: categorizeRelay(url),
connected: connectedRelays.includes(url)
}));
// Sort by first category, then by URL
relayList.sort((a, b) => {
const categoryA = a.categories[0] || '';
const categoryB = b.categories[0] || '';
if (categoryA !== categoryB) {
return categoryA.localeCompare(categoryB);
}
return a.url.localeCompare(b.url);
});
relays = relayList;
loading = false;
}
function handleRelayClick(url: string) {
// Extract hostname from relay URL (e.g., wss://thecitadel.nostr1.com -> thecitadel.nostr1.com)
// The route expects just the domain without protocol or port
let relayPath = url;
try {
const urlObj = new URL(url);
relayPath = urlObj.hostname;
} catch {
// If URL parsing fails, try to extract hostname manually
// Remove protocol (wss:// or ws://) and trailing slash
relayPath = url.replace(/^wss?:\/\//, '').replace(/\/$/, '');
// Remove port if present (route doesn't support ports in the parameter)
relayPath = relayPath.split(':')[0];
}
// Navigate to feed page with relay filter
goto(`/feed/relay/${relayPath}`);
}
onMount(async () => {
await nostrClient.initialize();
await loadRelays();
});
</script>
<Header />
<main class="container mx-auto px-4 py-8">
<div class="relay-page">
<h1 class="text-2xl font-bold mb-6 text-fog-text dark:text-fog-dark-text font-mono">/Relay</h1>
{#if loading}
<p class="text-fog-text dark:text-fog-dark-text">Loading relays...</p>
{:else}
<div class="relay-categories">
{#each ['Default', 'Profile', 'Thread Publish', 'GIF', 'Other'] as category}
{@const categoryRelays = relays.filter(r => r.categories.includes(category))}
{#if categoryRelays.length > 0}
<section class="relay-category">
<h2 class="category-title">{category}</h2>
<div class="relay-list">
{#each categoryRelays as relay}
<div
class="relay-item"
onclick={() => handleRelayClick(relay.url)}
role="button"
tabindex="0"
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleRelayClick(relay.url);
}
}}
>
<div class="relay-info">
<span class="relay-url">{relay.url}</span>
<div class="relay-meta">
{#if relay.categories.length > 1}
<span class="relay-categories-badge">
{relay.categories.length} categories
</span>
{/if}
<span class="relay-status" class:connected={relay.connected} class:disconnected={!relay.connected}>
{relay.connected ? '● Connected' : '○ Disconnected'}
</span>
</div>
</div>
<span class="relay-arrow"></span>
</div>
{/each}
</div>
</section>
{/if}
{/each}
</div>
{/if}
</div>
</main>
<style>
.relay-page {
max-width: 1000px;
margin: 0 auto;
}
.relay-categories {
display: flex;
flex-direction: column;
gap: 2rem;
}
.relay-category {
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.5rem;
padding: 1.5rem;
background: var(--fog-post, #ffffff);
}
:global(.dark) .relay-category {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
}
.category-title {
margin: 0 0 1rem 0;
font-size: 1.125rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
font-family: monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
}
:global(.dark) .category-title {
color: var(--fog-dark-text, #f9fafb);
}
.relay-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.relay-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-highlight, #f3f4f6);
cursor: pointer;
transition: all 0.2s;
font-family: monospace;
}
:global(.dark) .relay-item {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-highlight, #374151);
}
.relay-item:hover {
background: var(--fog-accent, #64748b);
color: white;
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .relay-item:hover {
background: var(--fog-dark-accent, #94a3b8);
border-color: var(--fog-dark-accent, #94a3b8);
}
.relay-item:focus {
outline: 2px solid var(--fog-accent, #64748b);
outline-offset: 2px;
}
.relay-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
flex: 1;
}
.relay-meta {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.relay-categories-badge {
font-size: 0.7rem;
opacity: 0.6;
color: var(--fog-text-light, #6b7280);
font-family: monospace;
}
:global(.dark) .relay-categories-badge {
color: var(--fog-dark-text-light, #9ca3af);
}
.relay-item:hover .relay-categories-badge {
color: white;
opacity: 0.8;
}
.relay-url {
font-size: 0.875rem;
color: var(--fog-text, #1f2937);
word-break: break-all;
}
:global(.dark) .relay-url {
color: var(--fog-dark-text, #f9fafb);
}
.relay-item:hover .relay-url {
color: white;
}
.relay-status {
font-size: 0.75rem;
opacity: 0.7;
}
.relay-status.connected {
color: #10b981;
}
.relay-status.disconnected {
color: #ef4444;
}
.relay-item:hover .relay-status {
color: white;
opacity: 1;
}
.relay-arrow {
font-size: 1.25rem;
opacity: 0.5;
margin-left: 1rem;
}
.relay-item:hover .relay-arrow {
opacity: 1;
}
</style>