RelayConnectModal.svelte raw
1 <script>
2 import { createEventDispatcher } from "svelte";
3 import { connectToRelay, normalizeWsUrl } from "./config.js";
4 import { relayInfo, relayConnectionStatus, relayUrl, savedRelays, saveRelay, removeRelay } from "./stores.js";
5
6 const dispatch = createEventDispatcher();
7
8 export let showModal = false;
9 export let isDarkTheme = false;
10
11 let urlInput = "";
12 let isConnecting = false;
13 let errorMessage = "";
14 let connectingUrl = "";
15
16 function closeModal() {
17 showModal = false;
18 errorMessage = "";
19 dispatch("close");
20 }
21
22 async function handleConnect(url = null) {
23 const targetUrl = url || urlInput.trim();
24 if (!targetUrl) {
25 errorMessage = "Please enter a relay URL";
26 return;
27 }
28
29 isConnecting = true;
30 connectingUrl = targetUrl;
31 errorMessage = "";
32
33 try {
34 const result = await connectToRelay(targetUrl);
35
36 if (result.success) {
37 // Save with the wss:// URL as the display name
38 const wsUrl = normalizeWsUrl(targetUrl);
39 saveRelay(targetUrl, wsUrl);
40 urlInput = ""; // Clear input on success
41 dispatch("connected", { info: result.info });
42 closeModal();
43 } else {
44 errorMessage = result.error || "Failed to connect";
45 }
46 } catch (error) {
47 errorMessage = error.message || "Connection failed";
48 } finally {
49 isConnecting = false;
50 connectingUrl = "";
51 }
52 }
53
54 async function handleAddRelay() {
55 const targetUrl = urlInput.trim();
56 if (!targetUrl) {
57 errorMessage = "Please enter a relay URL";
58 return;
59 }
60
61 isConnecting = true;
62 errorMessage = "";
63
64 try {
65 const result = await connectToRelay(targetUrl);
66
67 if (result.success) {
68 const wsUrl = normalizeWsUrl(targetUrl);
69 saveRelay(targetUrl, wsUrl);
70 urlInput = "";
71 dispatch("connected", { info: result.info });
72 // Don't close modal - stay open to manage relays
73 } else {
74 errorMessage = result.error || "Failed to connect";
75 }
76 } catch (error) {
77 errorMessage = error.message || "Connection failed";
78 } finally {
79 isConnecting = false;
80 }
81 }
82
83 function handleRemoveRelay(url, event) {
84 event.stopPropagation();
85 removeRelay(url);
86 }
87
88 function handleKeydown(event) {
89 if (event.key === "Enter" && !isConnecting) {
90 handleAddRelay();
91 } else if (event.key === "Escape") {
92 closeModal();
93 }
94 }
95
96 function isCurrentRelay(url) {
97 return $relayUrl === url && $relayConnectionStatus === "connected";
98 }
99
100 // Reset input when modal opens
101 $: if (showModal) {
102 urlInput = "";
103 errorMessage = "";
104 }
105 </script>
106
107 {#if showModal}
108 <!-- svelte-ignore a11y-click-events-have-key-events -->
109 <!-- svelte-ignore a11y-no-static-element-interactions -->
110 <div class="modal-overlay" on:click={closeModal}>
111 <div
112 class="modal"
113 class:dark={isDarkTheme}
114 on:click|stopPropagation
115 >
116 <div class="modal-header">
117 <h2>Relay Manager</h2>
118 <button class="close-btn" on:click={closeModal}>×</button>
119 </div>
120
121 <div class="modal-content">
122 <!-- Add new relay section at top -->
123 <div class="add-relay-section">
124 <div class="section-header">Add Relay</div>
125 <div class="input-row">
126 <input
127 type="text"
128 placeholder="wss://relay.example.com"
129 bind:value={urlInput}
130 on:keydown={handleKeydown}
131 disabled={isConnecting}
132 class="url-input"
133 />
134 <button
135 class="add-btn"
136 on:click={handleAddRelay}
137 disabled={isConnecting || !urlInput.trim()}
138 >
139 {#if isConnecting && !connectingUrl}
140 Adding...
141 {:else}
142 Add
143 {/if}
144 </button>
145 </div>
146 </div>
147
148 {#if errorMessage}
149 <div class="error-message">
150 {errorMessage}
151 </div>
152 {/if}
153
154 <!-- Saved relays list -->
155 <div class="saved-relays-section">
156 <div class="section-header">Saved Relays</div>
157 {#if $savedRelays.length > 0}
158 <div class="saved-relays-list">
159 {#each $savedRelays as relay}
160 <div
161 class="relay-item"
162 class:current={isCurrentRelay(relay.url)}
163 class:connecting={connectingUrl === relay.url}
164 >
165 <button
166 class="relay-connect-btn"
167 on:click={() => handleConnect(relay.url)}
168 disabled={isConnecting}
169 title="Click to connect"
170 >
171 <span class="relay-status-dot" class:connected={isCurrentRelay(relay.url)}></span>
172 <span class="relay-url-label">{relay.name}</span>
173 {#if isCurrentRelay(relay.url)}
174 <span class="current-badge">Connected</span>
175 {:else if connectingUrl === relay.url}
176 <span class="connecting-badge">Connecting...</span>
177 {/if}
178 </button>
179 <button
180 class="relay-remove-btn"
181 on:click={(e) => handleRemoveRelay(relay.url, e)}
182 title="Remove relay"
183 disabled={isConnecting}
184 >
185 Remove
186 </button>
187 </div>
188 {/each}
189 </div>
190 {:else}
191 <div class="empty-state">
192 No saved relays. Add one above to get started.
193 </div>
194 {/if}
195 </div>
196
197 <div class="button-group">
198 <button class="done-btn" on:click={closeModal}>
199 Done
200 </button>
201 </div>
202 </div>
203 </div>
204 </div>
205 {/if}
206
207 <style>
208 .modal-overlay {
209 position: fixed;
210 top: 0;
211 left: 0;
212 width: 100%;
213 height: 100%;
214 background-color: rgba(0, 0, 0, 0.5);
215 display: flex;
216 justify-content: center;
217 align-items: center;
218 z-index: 1000;
219 }
220
221 .modal {
222 background: var(--bg-color);
223 border-radius: 8px;
224 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
225 width: 90%;
226 max-width: 550px;
227 max-height: 90vh;
228 overflow-y: auto;
229 border: 1px solid var(--border-color);
230 }
231
232 .modal-header {
233 display: flex;
234 justify-content: space-between;
235 align-items: center;
236 padding: 16px 20px;
237 border-bottom: 1px solid var(--border-color);
238 }
239
240 .modal-header h2 {
241 margin: 0;
242 color: var(--text-color);
243 font-size: 1.25rem;
244 }
245
246 .close-btn {
247 background: none;
248 border: none;
249 font-size: 1.5rem;
250 cursor: pointer;
251 color: var(--text-color);
252 padding: 0;
253 width: 30px;
254 height: 30px;
255 display: flex;
256 align-items: center;
257 justify-content: center;
258 border-radius: 50%;
259 transition: background-color 0.2s;
260 }
261
262 .close-btn:hover {
263 background-color: var(--tab-hover-bg);
264 }
265
266 .modal-content {
267 padding: 16px 20px;
268 display: flex;
269 flex-direction: column;
270 gap: 16px;
271 }
272
273 .section-header {
274 font-size: 0.85rem;
275 color: var(--muted-foreground);
276 font-weight: 600;
277 text-transform: uppercase;
278 letter-spacing: 0.5px;
279 margin-bottom: 8px;
280 }
281
282 .add-relay-section {
283 padding-bottom: 16px;
284 border-bottom: 1px solid var(--border-color);
285 }
286
287 .input-row {
288 display: flex;
289 gap: 8px;
290 }
291
292 .url-input {
293 flex: 1;
294 padding: 10px 12px;
295 border: 1px solid var(--input-border);
296 border-radius: 6px;
297 font-size: 0.95rem;
298 font-family: monospace;
299 background: var(--bg-color);
300 color: var(--text-color);
301 }
302
303 .url-input:focus {
304 outline: none;
305 border-color: var(--primary);
306 }
307
308 .url-input:disabled {
309 opacity: 0.6;
310 cursor: not-allowed;
311 }
312
313 .add-btn {
314 padding: 10px 20px;
315 background: var(--primary);
316 color: white;
317 border: none;
318 border-radius: 6px;
319 cursor: pointer;
320 font-size: 0.95rem;
321 font-weight: 500;
322 white-space: nowrap;
323 transition: background-color 0.2s;
324 }
325
326 .add-btn:hover:not(:disabled) {
327 background: #00acc1;
328 }
329
330 .add-btn:disabled {
331 background: #ccc;
332 cursor: not-allowed;
333 }
334
335 .error-message {
336 padding: 10px 12px;
337 background: #fee2e2;
338 color: #dc2626;
339 border-radius: 6px;
340 font-size: 0.9rem;
341 }
342
343 .dark .error-message {
344 background: #450a0a;
345 color: #fca5a5;
346 }
347
348 .saved-relays-section {
349 flex: 1;
350 }
351
352 .saved-relays-list {
353 display: flex;
354 flex-direction: column;
355 gap: 6px;
356 }
357
358 .relay-item {
359 display: flex;
360 align-items: center;
361 gap: 8px;
362 padding: 4px;
363 border-radius: 6px;
364 background: var(--muted);
365 transition: background-color 0.2s;
366 }
367
368 .relay-item.current {
369 background: rgba(16, 185, 129, 0.15);
370 }
371
372 .relay-item.connecting {
373 background: rgba(234, 179, 8, 0.15);
374 }
375
376 .relay-connect-btn {
377 flex: 1;
378 display: flex;
379 align-items: center;
380 gap: 10px;
381 padding: 10px 12px;
382 background: transparent;
383 border: none;
384 cursor: pointer;
385 text-align: left;
386 border-radius: 4px;
387 transition: background-color 0.15s;
388 }
389
390 .relay-connect-btn:hover:not(:disabled) {
391 background: var(--tab-hover-bg);
392 }
393
394 .relay-connect-btn:disabled {
395 cursor: not-allowed;
396 opacity: 0.7;
397 }
398
399 .relay-status-dot {
400 width: 8px;
401 height: 8px;
402 border-radius: 50%;
403 background: var(--muted-foreground);
404 flex-shrink: 0;
405 }
406
407 .relay-status-dot.connected {
408 background: var(--success);
409 }
410
411 .relay-url-label {
412 flex: 1;
413 color: var(--text-color);
414 font-family: monospace;
415 font-size: 0.9rem;
416 white-space: nowrap;
417 overflow: hidden;
418 text-overflow: ellipsis;
419 }
420
421 .current-badge {
422 font-size: 0.7rem;
423 padding: 2px 8px;
424 background: var(--success);
425 color: white;
426 border-radius: 4px;
427 font-weight: 500;
428 flex-shrink: 0;
429 }
430
431 .connecting-badge {
432 font-size: 0.7rem;
433 padding: 2px 8px;
434 background: var(--warning);
435 color: white;
436 border-radius: 4px;
437 font-weight: 500;
438 flex-shrink: 0;
439 }
440
441 .relay-remove-btn {
442 padding: 6px 12px;
443 background: transparent;
444 border: 1px solid var(--border-color);
445 border-radius: 4px;
446 color: var(--muted-foreground);
447 cursor: pointer;
448 font-size: 0.8rem;
449 transition: background-color 0.2s, color 0.2s, border-color 0.2s;
450 flex-shrink: 0;
451 }
452
453 .relay-remove-btn:hover:not(:disabled) {
454 background: var(--danger);
455 border-color: var(--danger);
456 color: white;
457 }
458
459 .relay-remove-btn:disabled {
460 cursor: not-allowed;
461 opacity: 0.5;
462 }
463
464 .empty-state {
465 padding: 20px;
466 text-align: center;
467 color: var(--muted-foreground);
468 font-size: 0.9rem;
469 }
470
471 .button-group {
472 display: flex;
473 justify-content: flex-end;
474 margin-top: 8px;
475 padding-top: 16px;
476 border-top: 1px solid var(--border-color);
477 }
478
479 .done-btn {
480 padding: 10px 24px;
481 background: var(--primary);
482 color: white;
483 border: none;
484 border-radius: 6px;
485 cursor: pointer;
486 font-size: 0.95rem;
487 font-weight: 500;
488 transition: background-color 0.2s;
489 }
490
491 .done-btn:hover {
492 background: #00acc1;
493 }
494 </style>
495