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
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>
|
|
|