diff --git a/lib/data/nostr/nostr_service.dart b/lib/data/nostr/nostr_service.dart index db975e9..40fba7e 100644 --- a/lib/data/nostr/nostr_service.dart +++ b/lib/data/nostr/nostr_service.dart @@ -836,32 +836,73 @@ class NostrService { } // Attempt to connect to all added relays in parallel - // Connections happen in the background - if they fail, relays will be auto-disabled + // The connectRelay() method already maintains the connection via its internal listener + // We just need to wait for connections to be established + final connectionFutures = >[]; for (final relayUrl in addedRelayUrls) { - try { - // Connect in background - don't wait for it - connectRelay(relayUrl).then((stream) { - // Connection successful - stream will be handled by existing connection logic - Logger.info('Successfully connected to NIP-05 relay: $relayUrl'); - }).catchError((error) { - // Connection failed - relay will be auto-disabled by getRelays() logic - Logger.warning('Failed to connect to NIP-05 relay: $relayUrl - $error'); + connectionFutures.add( + Future(() async { try { - setRelayEnabled(relayUrl, false); - } catch (_) { - // Ignore errors + Logger.info('Connecting to NIP-05 relay: $relayUrl'); + + // Connect to the relay - this sets up the connection and maintains it + // The stream is returned but we don't need to use it - the connection + // is maintained by the internal listener in connectRelay() + await connectRelay(relayUrl).timeout( + const Duration(seconds: 5), + onTimeout: () { + throw Exception('Connection timeout'); + }, + ); + + // Wait for connection to be confirmed + // The service marks connection as established after 500ms if no errors occur + // or when the first message is received + // We'll wait up to 2 seconds, checking every 200ms + bool connected = false; + for (int i = 0; i < 10; i++) { + await Future.delayed(const Duration(milliseconds: 200)); + final relay = _relays.firstWhere( + (r) => r.url == relayUrl, + orElse: () => throw Exception('Relay not found'), + ); + if (relay.isConnected) { + connected = true; + Logger.info('Successfully connected to NIP-05 relay: $relayUrl'); + break; + } + } + + if (!connected) { + // Connection didn't establish within timeout - disable the relay + Logger.warning('Connection not established for NIP-05 relay: $relayUrl within 2 seconds - disabling'); + try { + disconnectRelay(relayUrl); + setRelayEnabled(relayUrl, false); + } catch (_) { + // Ignore errors + } + } + + // Note: We don't need to maintain a subscription to the stream here + // The connectRelay() method already has a listener that maintains the connection + // The stream is stored in _messageControllers and will remain active + } catch (e) { + // Connection failed - disable the relay + Logger.warning('Failed to connect to NIP-05 relay: $relayUrl - $e'); + try { + disconnectRelay(relayUrl); + setRelayEnabled(relayUrl, false); + } catch (_) { + // Ignore errors + } } - }); - } catch (e) { - // Connection attempt failed - disable the relay - Logger.warning('Failed to connect to NIP-05 relay: $relayUrl - $e'); - try { - setRelayEnabled(relayUrl, false); - } catch (_) { - // Ignore errors - } - } + }), + ); } + + // Wait for all connection attempts to complete (or timeout) + await Future.wait(connectionFutures, eagerError: false); Logger.info('Replaced ${existingRelays.length} relay(s) with $addedCount preferred relay(s) from NIP-05: $nip05 (all enabled and connecting)'); return addedCount; diff --git a/lib/data/session/session_service.dart b/lib/data/session/session_service.dart index e17f4af..7ec4625 100644 --- a/lib/data/session/session_service.dart +++ b/lib/data/session/session_service.dart @@ -5,6 +5,7 @@ import '../../core/logger.dart'; import '../../core/exceptions/session_exception.dart'; import '../../core/service_locator.dart'; import '../local/local_storage_service.dart'; +import '../local/models/item.dart'; import '../sync/sync_engine.dart'; import '../firebase/firebase_service.dart'; import '../nostr/nostr_service.dart'; @@ -365,8 +366,29 @@ class SessionService { Future _clearUserData(String userId) async { try { // Clear all data from local storage if it's the current user's storage + // But preserve app_settings (user-independent settings) if (_currentUser?.id == userId) { + // Save app_settings before clearing + Item? appSettings; + try { + appSettings = await _localStorage.getItem('app_settings'); + } catch (e) { + // If we can't read it, that's okay - it might not exist + Logger.debug('Could not read app_settings before clearing: $e'); + } + + // Clear all data await _localStorage.clearAllData(); + + // Restore app_settings if it existed + if (appSettings != null) { + try { + await _localStorage.insertItem(appSettings); + Logger.debug('Preserved app_settings during logout'); + } catch (e) { + Logger.warning('Failed to restore app_settings after logout: $e'); + } + } } // Delete user-specific recipes database file diff --git a/lib/ui/relay_management/relay_management_screen.dart b/lib/ui/relay_management/relay_management_screen.dart index 962826d..e040689 100644 --- a/lib/ui/relay_management/relay_management_screen.dart +++ b/lib/ui/relay_management/relay_management_screen.dart @@ -353,85 +353,91 @@ class _RelayListItem extends StatelessWidget { @override Widget build(BuildContext context) { return Card( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( children: [ - // Relay URL and status - Row( - children: [ - // Status indicator - // Enabled means connected - if it's enabled but not connected, it should be disabled - Container( - width: 12, - height: 12, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: relay.isConnected && relay.isEnabled - ? Colors.green - : Colors.grey, - ), - ), - const SizedBox(width: 8), - Expanded( - child: Text( + // Status indicator + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: relay.isConnected && relay.isEnabled + ? Colors.green + : Colors.grey, + ), + ), + const SizedBox(width: 8), + // URL and status text + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( relay.url, style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, + fontSize: 13, + fontWeight: FontWeight.w500, ), + overflow: TextOverflow.ellipsis, + maxLines: 1, ), - ), - ], - ), - const SizedBox(height: 8), - // Status text - // Enabled means connected - if it's enabled but not connected, it should be disabled - Text( - relay.isConnected && relay.isEnabled - ? 'Connected' - : 'Disabled', - style: TextStyle( - fontSize: 12, - color: relay.isConnected && relay.isEnabled - ? Colors.green - : Colors.grey, + const SizedBox(height: 2), + Text( + relay.isConnected && relay.isEnabled + ? 'Connected' + : 'Disabled', + style: TextStyle( + fontSize: 11, + color: relay.isConnected && relay.isEnabled + ? Colors.green + : Colors.grey, + ), + ), + ], ), ), - const SizedBox(height: 12), - // Action buttons + const SizedBox(width: 8), + // Toggle switch Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, children: [ - // Toggle switch - Row( - children: [ - Text( - relay.isEnabled ? 'On' : 'Off', - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - ), - ), - const SizedBox(width: 4), - Switch( - value: relay.isEnabled, - onChanged: (_) => onToggle(), - ), - ], + Text( + relay.isEnabled ? 'ON' : 'OFF', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: relay.isEnabled + ? Colors.green + : Colors.grey[600], + ), ), - const SizedBox(width: 8), - // Remove button - IconButton( - icon: const Icon(Icons.delete, size: 20), - color: Colors.red, - tooltip: 'Remove', - onPressed: onRemove, + const SizedBox(width: 4), + Transform.scale( + scale: 0.8, + child: Switch( + value: relay.isEnabled, + onChanged: (_) => onToggle(), + ), ), ], ), + const SizedBox(width: 4), + // Remove button + IconButton( + icon: const Icon(Icons.delete, size: 18), + color: Colors.red, + tooltip: 'Remove', + onPressed: onRemove, + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 32, + minHeight: 32, + ), + ), ], ), ),