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}>&times;</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