index.tsx raw

   1  /**
   2   * NRC Settings Component
   3   *
   4   * UI for managing Nostr Relay Connect (NRC) connections and listener settings.
   5   * Includes both:
   6   * - Listener mode: Allow other devices to connect to this one
   7   * - Client mode: Connect to and sync from other devices
   8   */
   9  
  10  import { useState, useCallback, useRef } from 'react'
  11  import { useTranslation } from 'react-i18next'
  12  import { useNRC } from '@/providers/NRCProvider'
  13  import { useNostr } from '@/providers/NostrProvider'
  14  import storage, { dispatchSettingsChanged } from '@/services/local-storage.service'
  15  import { Button } from '@/components/ui/button'
  16  import { Input } from '@/components/ui/input'
  17  import { Label } from '@/components/ui/label'
  18  import { Switch } from '@/components/ui/switch'
  19  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
  20  import {
  21    Dialog,
  22    DialogContent,
  23    DialogDescription,
  24    DialogFooter,
  25    DialogHeader,
  26    DialogTitle
  27  } from '@/components/ui/dialog'
  28  import {
  29    AlertDialog,
  30    AlertDialogAction,
  31    AlertDialogCancel,
  32    AlertDialogContent,
  33    AlertDialogDescription,
  34    AlertDialogFooter,
  35    AlertDialogHeader,
  36    AlertDialogTitle,
  37    AlertDialogTrigger
  38  } from '@/components/ui/alert-dialog'
  39  import {
  40    Link2,
  41    Plus,
  42    Trash2,
  43    Copy,
  44    Check,
  45    QrCode,
  46    Wifi,
  47    WifiOff,
  48    Users,
  49    Server,
  50    RefreshCw,
  51    Smartphone,
  52    Download,
  53    Camera,
  54    Zap
  55  } from 'lucide-react'
  56  import { NRCConnection, RemoteConnection } from '@/services/nrc'
  57  import QRCode from 'qrcode'
  58  import { Html5Qrcode } from 'html5-qrcode'
  59  
  60  export default function NRCSettings() {
  61    const { t } = useTranslation()
  62    const { pubkey } = useNostr()
  63    const {
  64      // Listener state
  65      isEnabled,
  66      isConnected,
  67      connections,
  68      activeSessions,
  69      rendezvousUrl,
  70      enable,
  71      disable,
  72      addConnection,
  73      removeConnection,
  74      getConnectionURI,
  75      setRendezvousUrl,
  76      // Client state
  77      remoteConnections,
  78      isSyncing,
  79      syncProgress,
  80      addRemoteConnection,
  81      removeRemoteConnection,
  82      testRemoteConnection,
  83      syncFromDevice,
  84      syncAllRemotes
  85    } = useNRC()
  86  
  87    // Listener state
  88    const [newConnectionLabel, setNewConnectionLabel] = useState('')
  89    const [newConnectionRendezvousUrl, setNewConnectionRendezvousUrl] = useState('')
  90    const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
  91    const [isQRDialogOpen, setIsQRDialogOpen] = useState(false)
  92    const [currentQRConnection, setCurrentQRConnection] = useState<NRCConnection | null>(null)
  93    const [currentQRUri, setCurrentQRUri] = useState('')
  94    const [qrDataUrl, setQrDataUrl] = useState('')
  95    const [copiedUri, setCopiedUri] = useState(false)
  96    const [isLoading, setIsLoading] = useState(false)
  97    const [enableError, setEnableError] = useState<string | null>(null)
  98    const [addConnectionError, setAddConnectionError] = useState<string | null>(null)
  99  
 100    // Client state
 101    const [connectionUri, setConnectionUri] = useState('')
 102    const [newRemoteLabel, setNewRemoteLabel] = useState('')
 103    const [isConnectDialogOpen, setIsConnectDialogOpen] = useState(false)
 104    const [isScannerOpen, setIsScannerOpen] = useState(false)
 105    const [scannerError, setScannerError] = useState('')
 106    const scannerRef = useRef<Html5Qrcode | null>(null)
 107    const scannerContainerRef = useRef<HTMLDivElement>(null)
 108  
 109    // Private config sync setting
 110    const [nrcOnlyConfigSync, setNrcOnlyConfigSync] = useState(storage.getNrcOnlyConfigSync())
 111  
 112    const handleToggleNrcOnlyConfig = useCallback((checked: boolean) => {
 113      storage.setNrcOnlyConfigSync(checked)
 114      setNrcOnlyConfigSync(checked)
 115      dispatchSettingsChanged()
 116    }, [])
 117  
 118    // Generate QR code when URI changes
 119    const generateQRCode = useCallback(async (uri: string) => {
 120      try {
 121        const dataUrl = await QRCode.toDataURL(uri, {
 122          width: 256,
 123          margin: 2,
 124          color: { dark: '#000000', light: '#ffffff' }
 125        })
 126        setQrDataUrl(dataUrl)
 127      } catch (error) {
 128        console.error('Failed to generate QR code:', error)
 129      }
 130    }, [])
 131  
 132    const handleToggleEnabled = useCallback(async () => {
 133      if (isEnabled) {
 134        disable()
 135        setEnableError(null)
 136      } else {
 137        setIsLoading(true)
 138        setEnableError(null)
 139        try {
 140          await enable()
 141        } catch (error) {
 142          const message = error instanceof Error ? error.message : 'Failed to enable NRC'
 143          setEnableError(message)
 144          console.error('Failed to enable NRC:', error)
 145        } finally {
 146          setIsLoading(false)
 147        }
 148      }
 149    }, [isEnabled, enable, disable])
 150  
 151    const handleAddConnection = useCallback(async () => {
 152      if (!newConnectionLabel.trim()) return
 153  
 154      setIsLoading(true)
 155      setAddConnectionError(null)
 156      try {
 157        // Use connection-specific URL if provided, otherwise uses global default
 158        const connectionRendezvousUrl = newConnectionRendezvousUrl.trim() || undefined
 159        const { uri, connection } = await addConnection(newConnectionLabel.trim(), connectionRendezvousUrl)
 160        setIsAddDialogOpen(false)
 161        setNewConnectionLabel('')
 162        setNewConnectionRendezvousUrl('')
 163  
 164        // Show QR code
 165        setCurrentQRConnection(connection)
 166        setCurrentQRUri(uri)
 167        await generateQRCode(uri)
 168        setIsQRDialogOpen(true)
 169      } catch (error) {
 170        const message = error instanceof Error ? error.message : 'Failed to add connection'
 171        setAddConnectionError(message)
 172        console.error('Failed to add connection:', error)
 173      } finally {
 174        setIsLoading(false)
 175      }
 176    }, [newConnectionLabel, newConnectionRendezvousUrl, addConnection])
 177  
 178    const handleShowQR = useCallback(
 179      async (connection: NRCConnection) => {
 180        try {
 181          const uri = getConnectionURI(connection)
 182          setCurrentQRConnection(connection)
 183          setCurrentQRUri(uri)
 184          await generateQRCode(uri)
 185          setIsQRDialogOpen(true)
 186        } catch (error) {
 187          console.error('Failed to get connection URI:', error)
 188        }
 189      },
 190      [getConnectionURI, generateQRCode]
 191    )
 192  
 193    const handleCopyUri = useCallback(async () => {
 194      try {
 195        await navigator.clipboard.writeText(currentQRUri)
 196        setCopiedUri(true)
 197        setTimeout(() => setCopiedUri(false), 2000)
 198      } catch (error) {
 199        console.error('Failed to copy URI:', error)
 200      }
 201    }, [currentQRUri])
 202  
 203    const handleRemoveConnection = useCallback(
 204      async (id: string) => {
 205        try {
 206          await removeConnection(id)
 207        } catch (error) {
 208          console.error('Failed to remove connection:', error)
 209        }
 210      },
 211      [removeConnection]
 212    )
 213  
 214    // ===== Client Handlers =====
 215    const handleAddRemoteConnection = useCallback(async () => {
 216      if (!connectionUri.trim() || !newRemoteLabel.trim()) return
 217  
 218      setIsLoading(true)
 219      try {
 220        await addRemoteConnection(connectionUri.trim(), newRemoteLabel.trim())
 221        setIsConnectDialogOpen(false)
 222        setConnectionUri('')
 223        setNewRemoteLabel('')
 224      } catch (error) {
 225        console.error('Failed to add remote connection:', error)
 226      } finally {
 227        setIsLoading(false)
 228      }
 229    }, [connectionUri, newRemoteLabel, addRemoteConnection])
 230  
 231    const handleRemoveRemoteConnection = useCallback(
 232      async (id: string) => {
 233        try {
 234          await removeRemoteConnection(id)
 235        } catch (error) {
 236          console.error('Failed to remove remote connection:', error)
 237        }
 238      },
 239      [removeRemoteConnection]
 240    )
 241  
 242    const handleSyncDevice = useCallback(
 243      async (id: string) => {
 244        try {
 245          await syncFromDevice(id)
 246        } catch (error) {
 247          console.error('Failed to sync from device:', error)
 248        }
 249      },
 250      [syncFromDevice]
 251    )
 252  
 253    const handleTestConnection = useCallback(
 254      async (id: string) => {
 255        try {
 256          await testRemoteConnection(id)
 257        } catch (error) {
 258          console.error('Failed to test connection:', error)
 259        }
 260      },
 261      [testRemoteConnection]
 262    )
 263  
 264    const handleSyncAll = useCallback(async () => {
 265      try {
 266        await syncAllRemotes()
 267      } catch (error) {
 268        console.error('Failed to sync all remotes:', error)
 269      }
 270    }, [syncAllRemotes])
 271  
 272    const startScanner = useCallback(async () => {
 273      if (!scannerContainerRef.current) return
 274  
 275      setScannerError('')
 276      try {
 277        const scanner = new Html5Qrcode('qr-scanner-container')
 278        scannerRef.current = scanner
 279  
 280        await scanner.start(
 281          { facingMode: 'environment' },
 282          {
 283            fps: 10,
 284            qrbox: { width: 250, height: 250 }
 285          },
 286          (decodedText) => {
 287            // Found a QR code
 288            if (decodedText.startsWith('nostr+relayconnect://')) {
 289              setConnectionUri(decodedText)
 290              stopScanner()
 291              setIsScannerOpen(false)
 292              setIsConnectDialogOpen(true)
 293            }
 294          },
 295          () => {
 296            // Ignore errors while scanning
 297          }
 298        )
 299      } catch (error) {
 300        console.error('Failed to start scanner:', error)
 301        setScannerError(error instanceof Error ? error.message : 'Failed to start camera')
 302      }
 303    }, [])
 304  
 305    const stopScanner = useCallback(() => {
 306      if (scannerRef.current) {
 307        scannerRef.current.stop().catch(() => {
 308          // Ignore errors when stopping
 309        })
 310        scannerRef.current = null
 311      }
 312    }, [])
 313  
 314    const handleOpenScanner = useCallback(() => {
 315      setIsScannerOpen(true)
 316      // Start scanner after dialog renders
 317      setTimeout(startScanner, 100)
 318    }, [startScanner])
 319  
 320    const handleCloseScanner = useCallback(() => {
 321      stopScanner()
 322      setIsScannerOpen(false)
 323      setScannerError('')
 324    }, [stopScanner])
 325  
 326    if (!pubkey) {
 327      return (
 328        <div className="text-muted-foreground text-sm">
 329          {t('Login required to use NRC')}
 330        </div>
 331      )
 332    }
 333  
 334    return (
 335      <div className="space-y-6">
 336        {/* Private Configuration Sync Toggle */}
 337        <div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg">
 338          <div className="space-y-1">
 339            <Label htmlFor="nrc-only-config" className="text-base font-medium">
 340              {t('Private Configuration Sync')}
 341            </Label>
 342            <p className="text-sm text-muted-foreground">
 343              {t('Only sync configurations between paired devices, not to public relays')}
 344            </p>
 345          </div>
 346          <Switch
 347            id="nrc-only-config"
 348            checked={nrcOnlyConfigSync}
 349            onCheckedChange={handleToggleNrcOnlyConfig}
 350          />
 351        </div>
 352  
 353        <Tabs defaultValue="listener" className="w-full">
 354          <TabsList className="grid w-full grid-cols-2">
 355            <TabsTrigger value="listener" className="gap-2">
 356              <Server className="w-4 h-4" />
 357              {t('Share')}
 358            </TabsTrigger>
 359            <TabsTrigger value="client" className="gap-2">
 360              <Smartphone className="w-4 h-4" />
 361              {t('Connect')}
 362            </TabsTrigger>
 363          </TabsList>
 364  
 365          {/* ===== LISTENER TAB ===== */}
 366          <TabsContent value="listener" className="space-y-6 mt-4">
 367            {/* Enable/Disable Toggle */}
 368            <div className="flex items-center justify-between">
 369              <div className="space-y-1">
 370                <Label htmlFor="nrc-enabled" className="text-base font-medium">
 371                  {t('Enable Relay Connect')}
 372                </Label>
 373                <p className="text-sm text-muted-foreground">
 374                  {t('Allow other devices to sync with this client')}
 375                </p>
 376              </div>
 377              <Switch
 378                id="nrc-enabled"
 379                checked={isEnabled}
 380                onCheckedChange={handleToggleEnabled}
 381                disabled={isLoading}
 382              />
 383            </div>
 384  
 385            {/* Enable Error */}
 386            {enableError && (
 387              <div className="p-3 bg-destructive/10 border border-destructive/20 rounded-lg text-sm text-destructive">
 388                {enableError}
 389              </div>
 390            )}
 391  
 392            {/* Status Indicator */}
 393            {isEnabled && (
 394              <div className="flex items-center gap-4 p-3 bg-muted/50 rounded-lg">
 395                <div className="flex items-center gap-2">
 396                  {isConnected ? (
 397                    <Wifi className="w-4 h-4 text-green-500" />
 398                  ) : (
 399                    <WifiOff className="w-4 h-4 text-yellow-500" />
 400                  )}
 401                  <span className="text-sm">
 402                    {isConnected ? t('Connected') : t('Connecting...')}
 403                  </span>
 404                </div>
 405                {activeSessions > 0 && (
 406                  <div className="flex items-center gap-2">
 407                    <Users className="w-4 h-4" />
 408                    <span className="text-sm">
 409                      {activeSessions} {t('active session(s)')}
 410                    </span>
 411                  </div>
 412                )}
 413              </div>
 414            )}
 415  
 416            {/* Rendezvous Relay */}
 417            <div className="space-y-2">
 418              <Label htmlFor="rendezvous-url" className="flex items-center gap-2">
 419                <Server className="w-4 h-4" />
 420                {t('Rendezvous Relay')}
 421              </Label>
 422              <Input
 423                id="rendezvous-url"
 424                value={rendezvousUrl}
 425                onChange={(e) => setRendezvousUrl(e.target.value)}
 426                placeholder="wss://relay.example.com"
 427                disabled={isEnabled}
 428              />
 429              {isEnabled && (
 430                <p className="text-xs text-muted-foreground">
 431                  {t('Disable NRC to change the relay')}
 432                </p>
 433              )}
 434            </div>
 435  
 436            {/* Connections List */}
 437            <div className="space-y-3">
 438              <div className="flex items-center justify-between">
 439                <Label className="flex items-center gap-2">
 440                  <Link2 className="w-4 h-4" />
 441                  {t('Authorized Devices')}
 442                </Label>
 443                <Button
 444                  variant="outline"
 445                  size="sm"
 446                  onClick={() => setIsAddDialogOpen(true)}
 447                  className="gap-1"
 448                >
 449                  <Plus className="w-4 h-4" />
 450                  {t('Add')}
 451                </Button>
 452              </div>
 453  
 454              {connections.length === 0 ? (
 455                <div className="text-sm text-muted-foreground p-4 text-center border border-dashed rounded-lg">
 456                  {t('No devices connected yet')}
 457                </div>
 458              ) : (
 459                <div className="space-y-2">
 460                  {connections.map((connection) => (
 461                    <div
 462                      key={connection.id}
 463                      className="flex items-center justify-between p-3 bg-muted/30 rounded-lg"
 464                    >
 465                      <div className="flex-1 min-w-0">
 466                        <div className="font-medium truncate">{connection.label}</div>
 467                        <div className="text-xs text-muted-foreground">
 468                          {new Date(connection.createdAt).toLocaleDateString()}
 469                        </div>
 470                      </div>
 471                      <div className="flex items-center gap-1">
 472                        <Button
 473                          variant="ghost"
 474                          size="icon"
 475                          onClick={() => handleShowQR(connection)}
 476                          title={t('Show QR Code')}
 477                        >
 478                          <QrCode className="w-4 h-4" />
 479                        </Button>
 480                        <AlertDialog>
 481                          <AlertDialogTrigger asChild>
 482                            <Button
 483                              variant="ghost"
 484                              size="icon"
 485                              className="text-destructive hover:text-destructive"
 486                              title={t('Remove')}
 487                            >
 488                              <Trash2 className="w-4 h-4" />
 489                            </Button>
 490                          </AlertDialogTrigger>
 491                          <AlertDialogContent>
 492                            <AlertDialogHeader>
 493                              <AlertDialogTitle>{t('Remove Device?')}</AlertDialogTitle>
 494                              <AlertDialogDescription>
 495                                {t('This will revoke access for "{{label}}". The device will no longer be able to sync.', {
 496                                  label: connection.label
 497                                })}
 498                              </AlertDialogDescription>
 499                            </AlertDialogHeader>
 500                            <AlertDialogFooter>
 501                              <AlertDialogCancel>{t('Cancel')}</AlertDialogCancel>
 502                              <AlertDialogAction
 503                                onClick={() => handleRemoveConnection(connection.id)}
 504                                className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
 505                              >
 506                                {t('Remove')}
 507                              </AlertDialogAction>
 508                            </AlertDialogFooter>
 509                          </AlertDialogContent>
 510                        </AlertDialog>
 511                      </div>
 512                    </div>
 513                  ))}
 514                </div>
 515              )}
 516            </div>
 517          </TabsContent>
 518  
 519          {/* ===== CLIENT TAB ===== */}
 520          <TabsContent value="client" className="space-y-6 mt-4">
 521            {/* Sync Progress */}
 522            {isSyncing && syncProgress && (
 523              <div className="p-3 bg-muted/50 rounded-lg space-y-2">
 524                <div className="flex items-center gap-2">
 525                  <RefreshCw className="w-4 h-4 animate-spin" />
 526                  <span className="text-sm font-medium">
 527                    {syncProgress.phase === 'connecting' && t('Connecting...')}
 528                    {syncProgress.phase === 'requesting' && t('Requesting events...')}
 529                    {syncProgress.phase === 'receiving' && t('Receiving events...')}
 530                    {syncProgress.phase === 'complete' && t('Sync complete')}
 531                    {syncProgress.phase === 'error' && t('Error')}
 532                  </span>
 533                </div>
 534                {syncProgress.eventsReceived > 0 && (
 535                  <div className="text-xs text-muted-foreground">
 536                    {t('{{count}} events received', { count: syncProgress.eventsReceived })}
 537                  </div>
 538                )}
 539                {syncProgress.message && syncProgress.phase === 'error' && (
 540                  <div className="text-xs text-destructive">{syncProgress.message}</div>
 541                )}
 542              </div>
 543            )}
 544  
 545            {/* Connect to Device */}
 546            <div className="space-y-3">
 547              <div className="flex items-center justify-between">
 548                <Label className="flex items-center gap-2">
 549                  <Download className="w-4 h-4" />
 550                  {t('Remote Devices')}
 551                </Label>
 552                <div className="flex gap-2">
 553                  <Button
 554                    variant="outline"
 555                    size="sm"
 556                    onClick={handleOpenScanner}
 557                    className="gap-1"
 558                  >
 559                    <Camera className="w-4 h-4" />
 560                    {t('Scan')}
 561                  </Button>
 562                  <Button
 563                    variant="outline"
 564                    size="sm"
 565                    onClick={() => setIsConnectDialogOpen(true)}
 566                    className="gap-1"
 567                  >
 568                    <Plus className="w-4 h-4" />
 569                    {t('Add')}
 570                  </Button>
 571                </div>
 572              </div>
 573  
 574              {remoteConnections.length === 0 ? (
 575                <div className="text-sm text-muted-foreground p-4 text-center border border-dashed rounded-lg">
 576                  {t('No remote devices configured')}
 577                </div>
 578              ) : (
 579                <div className="space-y-2">
 580                  {/* Sync All Button */}
 581                  {remoteConnections.length > 1 && (
 582                    <Button
 583                      variant="secondary"
 584                      size="sm"
 585                      onClick={handleSyncAll}
 586                      disabled={isSyncing}
 587                      className="w-full gap-2"
 588                    >
 589                      <RefreshCw className={`w-4 h-4 ${isSyncing ? 'animate-spin' : ''}`} />
 590                      {t('Sync All Devices')}
 591                    </Button>
 592                  )}
 593  
 594                  {remoteConnections.map((remote: RemoteConnection) => (
 595                    <div
 596                      key={remote.id}
 597                      className="flex items-center justify-between p-3 bg-muted/30 rounded-lg"
 598                    >
 599                      <div className="flex-1 min-w-0">
 600                        <div className="font-medium truncate">{remote.label}</div>
 601                        <div className="text-xs text-muted-foreground">
 602                          {remote.lastSync ? (
 603                            <>
 604                              {t('Last sync')}: {new Date(remote.lastSync).toLocaleString()}
 605                              {remote.eventCount !== undefined && (
 606                                <span className="ml-2">({remote.eventCount} {t('events')})</span>
 607                              )}
 608                            </>
 609                          ) : (
 610                            t('Never synced')
 611                          )}
 612                        </div>
 613                      </div>
 614                      <div className="flex items-center gap-1">
 615                        {/* Show Test button if never synced, Sync button otherwise */}
 616                        {!remote.lastSync ? (
 617                          <Button
 618                            variant="ghost"
 619                            size="icon"
 620                            onClick={() => handleTestConnection(remote.id)}
 621                            disabled={isSyncing}
 622                            title={t('Test Connection')}
 623                          >
 624                            <Zap className={`w-4 h-4 ${isSyncing ? 'animate-pulse' : ''}`} />
 625                          </Button>
 626                        ) : null}
 627                        <Button
 628                          variant="ghost"
 629                          size="icon"
 630                          onClick={() => handleSyncDevice(remote.id)}
 631                          disabled={isSyncing}
 632                          title={t('Sync')}
 633                        >
 634                          <RefreshCw className={`w-4 h-4 ${isSyncing ? 'animate-spin' : ''}`} />
 635                        </Button>
 636                        <AlertDialog>
 637                          <AlertDialogTrigger asChild>
 638                            <Button
 639                              variant="ghost"
 640                              size="icon"
 641                              className="text-destructive hover:text-destructive"
 642                              title={t('Remove')}
 643                            >
 644                              <Trash2 className="w-4 h-4" />
 645                            </Button>
 646                          </AlertDialogTrigger>
 647                          <AlertDialogContent>
 648                            <AlertDialogHeader>
 649                              <AlertDialogTitle>{t('Remove Remote Device?')}</AlertDialogTitle>
 650                              <AlertDialogDescription>
 651                                {t('This will remove "{{label}}" from your remote devices list.', {
 652                                  label: remote.label
 653                                })}
 654                              </AlertDialogDescription>
 655                            </AlertDialogHeader>
 656                            <AlertDialogFooter>
 657                              <AlertDialogCancel>{t('Cancel')}</AlertDialogCancel>
 658                              <AlertDialogAction
 659                                onClick={() => handleRemoveRemoteConnection(remote.id)}
 660                                className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
 661                              >
 662                                {t('Remove')}
 663                              </AlertDialogAction>
 664                            </AlertDialogFooter>
 665                          </AlertDialogContent>
 666                        </AlertDialog>
 667                      </div>
 668                    </div>
 669                  ))}
 670                </div>
 671              )}
 672            </div>
 673          </TabsContent>
 674        </Tabs>
 675  
 676        {/* ===== DIALOGS ===== */}
 677  
 678        {/* Add Connection Dialog (Listener) */}
 679        <Dialog open={isAddDialogOpen} onOpenChange={(open) => {
 680          setIsAddDialogOpen(open)
 681          if (open) {
 682            // Pre-populate with global rendezvous URL when opening
 683            setNewConnectionRendezvousUrl(rendezvousUrl)
 684            setAddConnectionError(null)
 685          } else {
 686            // Clear when closing
 687            setNewConnectionLabel('')
 688            setNewConnectionRendezvousUrl('')
 689            setAddConnectionError(null)
 690          }
 691        }}>
 692          <DialogContent>
 693            <DialogHeader>
 694              <DialogTitle>{t('Add Device')}</DialogTitle>
 695              <DialogDescription>
 696                {t('Create a connection URI to link another device')}
 697              </DialogDescription>
 698            </DialogHeader>
 699            <div className="space-y-4 py-4">
 700              <div className="space-y-2">
 701                <Label htmlFor="device-label">{t('Device Name')}</Label>
 702                <Input
 703                  id="device-label"
 704                  value={newConnectionLabel}
 705                  onChange={(e) => setNewConnectionLabel(e.target.value)}
 706                  placeholder={t('e.g., Phone, Laptop')}
 707                />
 708              </div>
 709              <div className="space-y-2">
 710                <Label htmlFor="device-rendezvous" className="flex items-center gap-2">
 711                  <Server className="w-4 h-4" />
 712                  {t('Rendezvous Relay')}
 713                </Label>
 714                <Input
 715                  id="device-rendezvous"
 716                  value={newConnectionRendezvousUrl}
 717                  onChange={(e) => setNewConnectionRendezvousUrl(e.target.value)}
 718                  placeholder="wss://relay.example.com"
 719                  onKeyDown={(e) => {
 720                    if (e.key === 'Enter') {
 721                      handleAddConnection()
 722                    }
 723                  }}
 724                />
 725                <p className="text-xs text-muted-foreground">
 726                  {t('Relay used to establish the connection')}
 727                </p>
 728              </div>
 729              {addConnectionError && (
 730                <div className="p-3 bg-destructive/10 border border-destructive/20 rounded-lg text-sm text-destructive">
 731                  {addConnectionError}
 732                </div>
 733              )}
 734            </div>
 735            <DialogFooter>
 736              <Button variant="outline" onClick={() => setIsAddDialogOpen(false)}>
 737                {t('Cancel')}
 738              </Button>
 739              <Button
 740                onClick={handleAddConnection}
 741                disabled={!newConnectionLabel.trim() || isLoading}
 742              >
 743                {t('Create')}
 744              </Button>
 745            </DialogFooter>
 746          </DialogContent>
 747        </Dialog>
 748  
 749        {/* QR Code Dialog */}
 750        <Dialog open={isQRDialogOpen} onOpenChange={setIsQRDialogOpen}>
 751          <DialogContent className="sm:max-w-md">
 752            <DialogHeader>
 753              <DialogTitle>{t('Connection QR Code')}</DialogTitle>
 754              <DialogDescription>
 755                {currentQRConnection && (
 756                  <>
 757                    {t('Scan this code with "{{label}}" to connect', {
 758                      label: currentQRConnection.label
 759                    })}
 760                  </>
 761                )}
 762              </DialogDescription>
 763            </DialogHeader>
 764            <div className="flex flex-col items-center gap-4 py-4">
 765              {qrDataUrl && (
 766                <div className="p-4 bg-white rounded-lg">
 767                  <img src={qrDataUrl} alt="Connection QR Code" className="w-64 h-64" />
 768                </div>
 769              )}
 770              <div className="w-full">
 771                <div className="flex items-center gap-2">
 772                  <Input
 773                    value={currentQRUri}
 774                    readOnly
 775                    className="font-mono text-xs"
 776                  />
 777                  <Button
 778                    variant="outline"
 779                    size="icon"
 780                    onClick={handleCopyUri}
 781                    title={t('Copy')}
 782                  >
 783                    {copiedUri ? (
 784                      <Check className="w-4 h-4 text-green-500" />
 785                    ) : (
 786                      <Copy className="w-4 h-4" />
 787                    )}
 788                  </Button>
 789                </div>
 790              </div>
 791            </div>
 792            <DialogFooter>
 793              <Button onClick={() => setIsQRDialogOpen(false)}>{t('Done')}</Button>
 794            </DialogFooter>
 795          </DialogContent>
 796        </Dialog>
 797  
 798        {/* Connect to Remote Dialog (Client) */}
 799        <Dialog open={isConnectDialogOpen} onOpenChange={setIsConnectDialogOpen}>
 800          <DialogContent>
 801            <DialogHeader>
 802              <DialogTitle>{t('Connect to Device')}</DialogTitle>
 803              <DialogDescription>
 804                {t('Enter a connection URI from another device to sync with it')}
 805              </DialogDescription>
 806            </DialogHeader>
 807            <div className="space-y-4 py-4">
 808              <div className="space-y-2">
 809                <Label htmlFor="connection-uri">{t('Connection URI')}</Label>
 810                <Input
 811                  id="connection-uri"
 812                  value={connectionUri}
 813                  onChange={(e) => setConnectionUri(e.target.value)}
 814                  placeholder="nostr+relayconnect://..."
 815                  className="font-mono text-xs"
 816                />
 817              </div>
 818              <div className="space-y-2">
 819                <Label htmlFor="remote-label">{t('Device Name')}</Label>
 820                <Input
 821                  id="remote-label"
 822                  value={newRemoteLabel}
 823                  onChange={(e) => setNewRemoteLabel(e.target.value)}
 824                  placeholder={t('e.g., Desktop, Main Phone')}
 825                  onKeyDown={(e) => {
 826                    if (e.key === 'Enter') {
 827                      handleAddRemoteConnection()
 828                    }
 829                  }}
 830                />
 831              </div>
 832            </div>
 833            <DialogFooter>
 834              <Button variant="outline" onClick={() => setIsConnectDialogOpen(false)}>
 835                {t('Cancel')}
 836              </Button>
 837              <Button
 838                onClick={handleAddRemoteConnection}
 839                disabled={!connectionUri.trim() || !newRemoteLabel.trim() || isLoading}
 840              >
 841                {t('Connect')}
 842              </Button>
 843            </DialogFooter>
 844          </DialogContent>
 845        </Dialog>
 846  
 847        {/* QR Scanner Dialog */}
 848        <Dialog open={isScannerOpen} onOpenChange={handleCloseScanner}>
 849          <DialogContent className="sm:max-w-md">
 850            <DialogHeader>
 851              <DialogTitle>{t('Scan QR Code')}</DialogTitle>
 852              <DialogDescription>
 853                {t('Point your camera at a connection QR code')}
 854              </DialogDescription>
 855            </DialogHeader>
 856            <div className="py-4">
 857              <div
 858                id="qr-scanner-container"
 859                ref={scannerContainerRef}
 860                className="w-full aspect-square bg-muted rounded-lg overflow-hidden"
 861              />
 862              {scannerError && (
 863                <div className="mt-2 text-sm text-destructive">{scannerError}</div>
 864              )}
 865            </div>
 866            <DialogFooter>
 867              <Button variant="outline" onClick={handleCloseScanner}>
 868                {t('Cancel')}
 869              </Button>
 870            </DialogFooter>
 871          </DialogContent>
 872        </Dialog>
 873      </div>
 874    )
 875  }
 876