diff --git a/README.md b/README.md index e65f5d5..dac55ef 100644 --- a/README.md +++ b/README.md @@ -11,59 +11,6 @@ A modular, offline-first Flutter boilerplate for apps that store, sync, and shar - Modular navigation architecture with testable components - Comprehensive UI tests for navigation and route guards -## Phase 7 - Firebase Layer - -- Optional Firebase integration for cloud sync, storage, auth, push notifications, and analytics -- Modular design - can be enabled or disabled without affecting other modules -- Offline-first behavior maintained when Firebase is disabled -- Integration with session management and local storage -- Comprehensive unit tests - -## Phase 6 - User Session Management - -- User login, logout, and session switching -- Per-user data isolation with separate storage paths -- Cache clearing on logout -- Integration with local storage and sync engine -- Comprehensive unit tests - -## Phase 5 - Relay Management UI - -- User interface for managing Nostr relays -- View, add, remove, and monitor relay health -- Manual sync trigger integration -- Modular controller-based architecture -- Comprehensive UI tests - -## Phase 4 - Sync Engine - -- Coordinates data synchronization between local storage, Immich, and Nostr -- Conflict resolution strategies (useLocal, useRemote, useLatest, merge) -- Offline queue with automatic retry -- Priority-based operation processing -- Comprehensive unit and integration tests - -## Phase 3 - Nostr Integration - -- Nostr protocol service for decentralized metadata synchronization -- Keypair generation and event publishing -- Multi-relay support for metadata syncing -- Comprehensive unit tests - -## Phase 2 - Immich Integration - -- Immich API service for uploading and fetching images -- Automatic metadata storage in local database -- Offline-first behavior with local caching -- Comprehensive unit tests - -## Phase 1 - Local Storage & Caching - -- Local storage service with SQLite database -- CRUD operations for items -- Image caching functionality -- Comprehensive unit tests - ## Quick Start ```bash diff --git a/lib/data/nostr/nostr_service.dart b/lib/data/nostr/nostr_service.dart index f56205d..e625c29 100644 --- a/lib/data/nostr/nostr_service.dart +++ b/lib/data/nostr/nostr_service.dart @@ -87,7 +87,17 @@ class NostrService { } /// Gets the list of configured relays. + /// + /// Automatically disables any relays that are enabled but not connected, + /// since enabled should always mean connected. List getRelays() { + // Ensure enabled relays are actually connected + // If a relay is enabled but not connected, disable it + for (final relay in _relays) { + if (relay.isEnabled && !relay.isConnected) { + relay.isEnabled = false; + } + } return List.unmodifiable(_relays); } @@ -120,8 +130,11 @@ class NostrService { // Wrap in try-catch to ensure synchronous errors are caught WebSocketChannel channel; try { + Logger.info('Creating WebSocket connection to: $relayUrl'); channel = WebSocketChannel.connect(Uri.parse(relayUrl)); + Logger.debug('WebSocketChannel created for: $relayUrl'); } catch (e) { + Logger.error('Failed to create WebSocketChannel for: $relayUrl', e); throw NostrException('Failed to connect to relay: $e'); } _connections[relayUrl] = channel; @@ -129,12 +142,33 @@ class NostrService { final controller = StreamController>.broadcast(); _messageControllers[relayUrl] = controller; - // Don't set isConnected = true immediately - wait for actual connection - // The connection might fail asynchronously + // Mark connection as established after a short delay if no errors occur + // WebSocket connection is established when channel is created successfully + // We'll wait a bit to catch any immediate connection errors + bool connectionConfirmed = false; + bool hasError = false; + + // Set up error tracking before listening + Timer? connectionTimer; + connectionTimer = Timer(const Duration(milliseconds: 500), () { + if (!hasError && !connectionConfirmed) { + // No errors occurred, connection is established + connectionConfirmed = true; + relay.isConnected = true; + Logger.info('Connection confirmed for relay: $relayUrl (no errors after 500ms)'); + } + }); // Listen for messages channel.stream.listen( (message) { + // First message received - connection is confirmed + if (!connectionConfirmed) { + connectionConfirmed = true; + relay.isConnected = true; + Logger.info('Connection confirmed for relay: $relayUrl (first message received)'); + connectionTimer?.cancel(); + } try { final data = jsonDecode(message as String); if (data is List && data.isNotEmpty) { @@ -168,11 +202,23 @@ class NostrService { } }, onError: (error) { + hasError = true; + connectionTimer?.cancel(); + Logger.error('WebSocket error for relay: $relayUrl', error); relay.isConnected = false; + // Automatically disable relay when connection error occurs + relay.isEnabled = false; + Logger.warning('Relay $relayUrl disabled due to connection error'); controller.addError(NostrException('Relay error: $error')); }, onDone: () { + hasError = true; + connectionTimer?.cancel(); + Logger.warning('WebSocket stream closed for relay: $relayUrl'); relay.isConnected = false; + // Automatically disable relay when connection closes + relay.isEnabled = false; + Logger.warning('Relay $relayUrl disabled due to stream closure'); controller.close(); }, ); diff --git a/lib/ui/home/home_screen.dart b/lib/ui/home/home_screen.dart index 798c6cb..51e51c6 100644 --- a/lib/ui/home/home_screen.dart +++ b/lib/ui/home/home_screen.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import '../../core/service_locator.dart'; -import '../../data/local/local_storage_service.dart'; import '../../data/local/models/item.dart'; /// Home screen showing local storage and cached content. diff --git a/lib/ui/immich/immich_screen.dart b/lib/ui/immich/immich_screen.dart index 4c0f0d8..0e7107d 100644 --- a/lib/ui/immich/immich_screen.dart +++ b/lib/ui/immich/immich_screen.dart @@ -4,8 +4,6 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import '../../core/logger.dart'; import '../../core/service_locator.dart'; -import '../../data/local/local_storage_service.dart'; -import '../../data/immich/immich_service.dart'; import '../../data/immich/models/immich_asset.dart'; /// Screen for Immich media integration. diff --git a/lib/ui/nostr_events/nostr_events_screen.dart b/lib/ui/nostr_events/nostr_events_screen.dart index dadd294..b746fb4 100644 --- a/lib/ui/nostr_events/nostr_events_screen.dart +++ b/lib/ui/nostr_events/nostr_events_screen.dart @@ -1,8 +1,5 @@ import 'package:flutter/material.dart'; import '../../core/service_locator.dart'; -import '../../data/nostr/nostr_service.dart'; -import '../../data/sync/sync_engine.dart'; -import '../../data/session/session_service.dart'; import '../../data/nostr/models/nostr_keypair.dart'; import '../../data/nostr/models/nostr_event.dart'; import '../relay_management/relay_management_screen.dart'; diff --git a/lib/ui/relay_management/relay_management_controller.dart b/lib/ui/relay_management/relay_management_controller.dart index cca1672..05a8b85 100644 --- a/lib/ui/relay_management/relay_management_controller.dart +++ b/lib/ui/relay_management/relay_management_controller.dart @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart'; import '../../data/nostr/nostr_service.dart'; import '../../data/nostr/models/nostr_relay.dart'; import '../../data/sync/sync_engine.dart'; +import '../../core/logger.dart'; /// Controller for managing Nostr relay UI state and operations. /// @@ -49,7 +50,13 @@ class RelayManagementController extends ChangeNotifier { /// Loads relays from the Nostr service. void _loadRelays() { - _relays = nostrService.getRelays(); + // Create a new list with new relay objects to ensure Flutter detects the change + final serviceRelays = nostrService.getRelays(); + _relays = serviceRelays.map((relay) => NostrRelay( + url: relay.url, + isConnected: relay.isConnected, + isEnabled: relay.isEnabled, + )).toList(); notifyListeners(); } @@ -57,8 +64,10 @@ class RelayManagementController extends ChangeNotifier { /// /// [relayUrl] - The WebSocket URL of the relay. /// - /// Returns true if the relay was added successfully, false if it already exists. - bool addRelay(String relayUrl) { + /// Tests the connection and enables the relay if successful. + /// + /// Returns true if the relay was added successfully, false if it already exists or connection failed. + Future addRelay(String relayUrl) async { try { _error = null; @@ -69,9 +78,105 @@ class RelayManagementController extends ChangeNotifier { return false; } + // Add the relay (it will be disabled by default) nostrService.addRelay(relayUrl); _loadRelays(); - return true; + + // Test the connection + Logger.info('Testing connection to relay: $relayUrl'); + try { + final stream = await nostrService + .connectRelay(relayUrl) + .timeout( + const Duration(seconds: 3), + onTimeout: () { + Logger.warning('Connection timeout for relay: $relayUrl'); + throw Exception('Connection timeout'); + }, + ); + Logger.debug('WebSocket stream created for relay: $relayUrl'); + + // Wait a bit to see if connection actually works (check for errors) + Logger.debug('Setting up stream listener for relay: $relayUrl'); + final completer = Completer(); + late StreamSubscription subscription; + bool gotError = false; + + subscription = stream.listen( + (data) { + // Got data - connection is working + Logger.info('Received data from relay $relayUrl during add - connection confirmed'); + if (!completer.isCompleted) { + completer.complete(true); + } + }, + onError: (error) { + // Connection error occurred + Logger.error('Stream error for relay $relayUrl during add', error); + gotError = true; + if (!completer.isCompleted) { + completer.complete(false); + } + }, + onDone: () { + // Stream closed - connection failed + Logger.warning('Stream closed for relay $relayUrl during add - connection failed'); + if (!completer.isCompleted) { + completer.complete(false); + } + }, + ); + + // Wait for either data (success) or error/timeout (failure) + Logger.debug('Waiting for connection confirmation for relay: $relayUrl (timeout: 2 seconds)'); + final connected = await completer.future.timeout( + const Duration(seconds: 2), + onTimeout: () { + Logger.warning('Timeout waiting for connection confirmation for relay: $relayUrl during add'); + subscription.cancel(); + // If no error occurred but no data either, check if relay is marked as connected + _loadRelays(); + final relay = _relays.firstWhere( + (r) => r.url == relayUrl, + orElse: () => throw Exception('Relay not found'), + ); + Logger.debug('Relay $relayUrl connection state during add: isConnected=${relay.isConnected}, isEnabled=${relay.isEnabled}'); + return relay.isConnected; + }, + ); + + subscription.cancel(); + + if (connected && !gotError) { + // Connection successful - enable the relay + Logger.info('Connection successful for relay: $relayUrl - enabling relay'); + nostrService.setRelayEnabled(relayUrl, true); + _loadRelays(); + return true; + } else { + // Connection failed - leave it disabled + Logger.warning('Connection failed for relay: $relayUrl (connected=$connected, gotError=$gotError) - leaving disabled'); + nostrService.disconnectRelay(relayUrl); + nostrService.setRelayEnabled(relayUrl, false); + _loadRelays(); + _error = 'Failed to connect to relay'; + notifyListeners(); + return false; + } + } catch (e) { + // Connection test failed - leave relay disabled + Logger.error('Exception during connection test for relay: $relayUrl', e); + try { + nostrService.disconnectRelay(relayUrl); + nostrService.setRelayEnabled(relayUrl, false); + _loadRelays(); + } catch (_) { + // Ignore disconnect errors + } + _error = 'Failed to connect to relay: ${e.toString().replaceAll('Exception: ', '')}'; + notifyListeners(); + return false; + } } catch (e) { _error = 'Failed to add relay: $e'; notifyListeners(); @@ -141,10 +246,13 @@ class RelayManagementController extends ChangeNotifier { stream.listen(null).cancel(); return true; } catch (e) { - // Connection failed - disconnect to mark as unhealthy + // Connection failed - disconnect and disable the relay try { nostrService.disconnectRelay(relayUrl); + nostrService.setRelayEnabled(relayUrl, false); + // Reload and notify to update UI _loadRelays(); + notifyListeners(); } catch (_) { // Ignore disconnect errors } @@ -155,12 +263,113 @@ class RelayManagementController extends ChangeNotifier { /// Toggles a relay on/off (enables/disables it). /// /// [relayUrl] - The URL of the relay to toggle. - void toggleRelay(String relayUrl) { + /// + /// When enabling, automatically attempts to connect to the relay. + /// If connection fails, automatically disables the relay (toggle moves back to OFF). + /// Always attempts to reconnect when toggling on, even if previously failed. + /// + /// The toggle responds immediately for better UX, then reverts if connection fails. + Future toggleRelay(String relayUrl) async { try { _error = null; final relay = _relays.firstWhere((r) => r.url == relayUrl); - nostrService.setRelayEnabled(relayUrl, !relay.isEnabled); - _loadRelays(); + final newEnabledState = !relay.isEnabled; + + // If disabling, just disconnect (no test needed) - update UI immediately + if (!newEnabledState) { + try { + nostrService.setRelayEnabled(relayUrl, false); + nostrService.disconnectRelay(relayUrl); + _loadRelays(); + } catch (_) { + // Ignore disconnect errors + } + return; + } + + // If enabling, update UI immediately (optimistic update) + // Disconnect first to ensure a fresh connection attempt + Logger.debug('Disconnecting existing connection for relay: $relayUrl (if any)'); + try { + nostrService.disconnectRelay(relayUrl); + } catch (_) { + // Ignore if not connected + } + + // Enable the relay immediately and update UI (optimistic update) + Logger.info('Toggling relay ON: $relayUrl - updating UI immediately'); + nostrService.setRelayEnabled(relayUrl, true); + + // Update UI immediately by updating the relay in our list directly + // This bypasses the auto-disable logic in getRelays() during connection attempt + final relayIndex = _relays.indexWhere((r) => r.url == relayUrl); + if (relayIndex != -1) { + _relays[relayIndex] = NostrRelay( + url: relayUrl, + isConnected: _relays[relayIndex].isConnected, + isEnabled: true, + ); + notifyListeners(); // Update UI immediately + } + + // Now attempt to connect in the background + // If connection fails, we'll toggle it back to OFF + Logger.info('Attempting to connect to relay: $relayUrl (background)'); + try { + final stream = await nostrService + .connectRelay(relayUrl) + .timeout( + const Duration(seconds: 5), + onTimeout: () { + Logger.warning('Connection timeout for relay: $relayUrl (5 seconds)'); + throw Exception('Connection timeout'); + }, + ); + Logger.debug('WebSocket stream created for relay: $relayUrl'); + + // Wait a short time to see if connection is established (no errors) + // The service marks connection as established after 500ms if no errors occur + Logger.debug('Waiting for connection confirmation for relay: $relayUrl (timeout: 1 second)'); + await Future.delayed(const Duration(seconds: 1)); + + // Cancel the stream subscription to clean up + stream.listen(null).cancel(); + + // Check if connection was established (no errors occurred) + _loadRelays(); + final updatedRelay = _relays.firstWhere( + (r) => r.url == relayUrl, + orElse: () => throw Exception('Relay not found'), + ); + + Logger.debug('Relay $relayUrl connection state: isConnected=${updatedRelay.isConnected}, isEnabled=${updatedRelay.isEnabled}'); + + if (!updatedRelay.isConnected) { + // Connection failed - toggle back to OFF + Logger.warning('Connection failed for relay: $relayUrl - toggling back to OFF'); + nostrService.disconnectRelay(relayUrl); + nostrService.setRelayEnabled(relayUrl, false); + _loadRelays(); + _error = 'Failed to connect to relay'; + notifyListeners(); + } else { + // Connection successful - keep it enabled and connected + Logger.info('Connection successful for relay: $relayUrl - keeping enabled'); + _loadRelays(); + } + } catch (e) { + // Connection failed - toggle back to OFF + Logger.error('Exception during toggle connection for relay: $relayUrl', e); + try { + nostrService.disconnectRelay(relayUrl); + nostrService.setRelayEnabled(relayUrl, false); + _loadRelays(); + _error = 'Failed to connect to relay: ${e.toString().replaceAll('Exception: ', '')}'; + notifyListeners(); + } catch (_) { + // Ignore disconnect errors + } + } } catch (e) { _error = 'Failed to toggle relay: $e'; notifyListeners(); @@ -168,13 +377,140 @@ class RelayManagementController extends ChangeNotifier { } /// Toggles all relays on/off. - void toggleAllRelays() { + /// + /// When enabling, automatically attempts to connect to all relays. + /// Always attempts to reconnect when toggling on, even if previously failed. + Future toggleAllRelays() async { try { _error = null; final allEnabled = _relays.every((r) => r.isEnabled); - nostrService.setAllRelaysEnabled(!allEnabled); + final newEnabledState = !allEnabled; + + // If disabling, just disconnect all (no test needed) + if (!newEnabledState) { + Logger.info('Toggling all relays OFF'); + try { + nostrService.setAllRelaysEnabled(false); + for (final relay in _relays) { + try { + nostrService.disconnectRelay(relay.url); + } catch (_) { + // Ignore disconnect errors + } + } + _loadRelays(); + } catch (_) { + // Ignore errors + } + return; + } + + // If enabling, first ensure all are enabled, then attempt to reconnect + Logger.info('Toggling all relays ON - attempting to connect to all'); + + // Disconnect all first to ensure fresh connection attempts + final currentRelayUrls = _relays.map((r) => r.url).toList(); + for (final relayUrl in currentRelayUrls) { + try { + nostrService.disconnectRelay(relayUrl); + } catch (_) { + // Ignore if not connected + } + } + + // Enable all relays immediately and update UI (optimistic update) + nostrService.setAllRelaysEnabled(true); + + // Update UI immediately by updating relays in our list directly + // This bypasses the auto-disable logic in getRelays() during connection attempts + for (var i = 0; i < _relays.length; i++) { + _relays[i] = NostrRelay( + url: _relays[i].url, + isConnected: _relays[i].isConnected, + isEnabled: true, + ); + } + notifyListeners(); // Update UI immediately + + // Capture relay URLs before starting connections (list might change) + final relayUrls = _relays.map((r) => r.url).toList(); + + // Now attempt to connect to all relays in parallel + final futures = >[]; + for (final relayUrl in relayUrls) { + futures.add( + Future(() async { + try { + Logger.info('Attempting to connect to relay: $relayUrl'); + final stream = await nostrService + .connectRelay(relayUrl) + .timeout( + const Duration(seconds: 5), + onTimeout: () { + Logger.warning('Connection timeout for relay: $relayUrl (5 seconds)'); + throw Exception('Connection timeout'); + }, + ); + Logger.debug('WebSocket stream created for relay: $relayUrl'); + + // Wait a short time to see if connection is established (no errors) + // The service marks connection as established after 500ms if no errors occur + Logger.debug('Waiting for connection confirmation for relay: $relayUrl (timeout: 1 second)'); + await Future.delayed(const Duration(seconds: 1)); + + // Cancel the stream subscription to clean up + stream.listen(null).cancel(); + + // Check if connection was established + _loadRelays(); + final updatedRelay = _relays.firstWhere( + (r) => r.url == relayUrl, + orElse: () => throw Exception('Relay not found'), + ); + + Logger.debug('Relay $relayUrl connection state: isConnected=${updatedRelay.isConnected}, isEnabled=${updatedRelay.isEnabled}'); + + if (!updatedRelay.isConnected) { + // Connection failed - disable the relay + Logger.warning('Connection failed for relay: $relayUrl'); + throw Exception('Connection failed'); + } + + // Connection successful + Logger.info('Connection successful for relay: $relayUrl'); + } catch (e) { + // Connection failed - automatically disable the relay + Logger.error('Exception during connection for relay: $relayUrl', e); + try { + nostrService.disconnectRelay(relayUrl); + nostrService.setRelayEnabled(relayUrl, false); + _loadRelays(); + } catch (_) { + // Ignore disconnect errors + } + } + }).catchError((error) { + // Final safety net - ensure no exceptions escape + Logger.error('Error in toggleAllRelays for relay: $relayUrl', error); + try { + nostrService.disconnectRelay(relayUrl); + nostrService.setRelayEnabled(relayUrl, false); + _loadRelays(); + } catch (_) { + // Ignore all errors + } + return Future.value(); + }), + ); + } + + // Wait for all connection attempts to complete (or fail gracefully) + Logger.info('Waiting for all ${futures.length} relay connection attempts to complete'); + await Future.wait(futures, eagerError: false); + Logger.info('All relay connection attempts completed'); _loadRelays(); } catch (e) { + Logger.error('Failed to toggle all relays', e); _error = 'Failed to toggle all relays: $e'; notifyListeners(); } @@ -213,10 +549,13 @@ class RelayManagementController extends ChangeNotifier { // Cancel the stream subscription to clean up stream.listen(null).cancel(); } catch (e) { - // Connection failed - disconnect to mark as unhealthy + // Connection failed - disconnect and disable to mark as unhealthy try { nostrService.disconnectRelay(relay.url); + nostrService.setRelayEnabled(relay.url, false); + // Reload and notify to update UI _loadRelays(); + notifyListeners(); } catch (_) { // Ignore disconnect errors } @@ -225,10 +564,13 @@ class RelayManagementController extends ChangeNotifier { } } catch (e) { // Catch all exceptions - connection failures are expected in tests - // Disconnect relay to mark as unhealthy + // Disconnect and disable relay to mark as unhealthy try { nostrService.disconnectRelay(relay.url); + nostrService.setRelayEnabled(relay.url, false); + // Reload and notify to update UI _loadRelays(); + notifyListeners(); } catch (_) { // Ignore disconnect errors } @@ -238,6 +580,7 @@ class RelayManagementController extends ChangeNotifier { // Final safety net - ensure no exceptions escape try { nostrService.disconnectRelay(relay.url); + nostrService.setRelayEnabled(relay.url, false); _loadRelays(); } catch (_) { // Ignore all errors diff --git a/lib/ui/relay_management/relay_management_screen.dart b/lib/ui/relay_management/relay_management_screen.dart index 73c8fa1..9a5d5f9 100644 --- a/lib/ui/relay_management/relay_management_screen.dart +++ b/lib/ui/relay_management/relay_management_screen.dart @@ -21,7 +21,6 @@ class RelayManagementScreen extends StatefulWidget { class _RelayManagementScreenState extends State { final TextEditingController _urlController = TextEditingController(); - final Map _testingRelays = {}; @override void dispose() { @@ -29,31 +28,6 @@ class _RelayManagementScreenState extends State { super.dispose(); } - Future _handleTestRelay(String relayUrl) async { - setState(() { - _testingRelays[relayUrl] = true; - }); - - final success = await widget.controller.testRelay(relayUrl); - - setState(() { - _testingRelays[relayUrl] = false; - }); - - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - success - ? 'Relay test successful' - : 'Relay test failed', - ), - duration: const Duration(seconds: 2), - ), - ); - } - } - @override Widget build(BuildContext context) { return Scaffold( @@ -119,7 +93,9 @@ class _RelayManagementScreenState extends State { child: ElevatedButton.icon( onPressed: widget.controller.relays.isEmpty ? null - : widget.controller.toggleAllRelays, + : () async { + await widget.controller.toggleAllRelays(); + }, icon: const Icon(Icons.power_settings_new), label: Text( widget.controller.relays.isNotEmpty && @@ -148,17 +124,31 @@ class _RelayManagementScreenState extends State { ), const SizedBox(width: 8), ElevatedButton.icon( - onPressed: () { + onPressed: () async { final url = _urlController.text.trim(); if (url.isNotEmpty) { - if (widget.controller.addRelay(url)) { - _urlController.clear(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Relay added successfully'), - duration: Duration(seconds: 2), - ), - ); + final success = await widget.controller.addRelay(url); + if (mounted) { + if (success) { + _urlController.clear(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Relay added and connected successfully'), + backgroundColor: Colors.green, + duration: Duration(seconds: 2), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + widget.controller.error ?? 'Failed to connect to relay', + ), + backgroundColor: Colors.orange, + duration: const Duration(seconds: 3), + ), + ); + } } } }, @@ -208,9 +198,9 @@ class _RelayManagementScreenState extends State { final relay = widget.controller.relays[index]; return _RelayListItem( relay: relay, - isTesting: _testingRelays[relay.url] ?? false, - onTest: () => _handleTestRelay(relay.url), - onToggle: () => widget.controller.toggleRelay(relay.url), + onToggle: () async { + await widget.controller.toggleRelay(relay.url); + }, onRemove: () { widget.controller.removeRelay(relay.url); ScaffoldMessenger.of(context).showSnackBar( @@ -237,12 +227,6 @@ class _RelayListItem extends StatelessWidget { /// The relay to display. final NostrRelay relay; - /// Whether the relay is currently being tested. - final bool isTesting; - - /// Callback when test is pressed. - final VoidCallback onTest; - /// Callback when toggle is pressed. final VoidCallback onToggle; @@ -251,8 +235,6 @@ class _RelayListItem extends StatelessWidget { const _RelayListItem({ required this.relay, - required this.isTesting, - required this.onTest, required this.onToggle, required this.onRemove, }); @@ -270,16 +252,15 @@ class _RelayListItem extends StatelessWidget { 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 + color: relay.isConnected && relay.isEnabled ? Colors.green - : relay.isEnabled - ? Colors.orange - : Colors.grey, + : Colors.grey, ), ), const SizedBox(width: 8), @@ -296,19 +277,16 @@ class _RelayListItem extends StatelessWidget { ), const SizedBox(height: 8), // Status text + // Enabled means connected - if it's enabled but not connected, it should be disabled Text( - relay.isConnected + relay.isConnected && relay.isEnabled ? 'Connected' - : relay.isEnabled - ? 'Enabled (not connected)' - : 'Disabled', + : 'Disabled', style: TextStyle( fontSize: 12, - color: relay.isConnected + color: relay.isConnected && relay.isEnabled ? Colors.green - : relay.isEnabled - ? Colors.orange - : Colors.grey, + : Colors.grey, ), ), const SizedBox(height: 12), @@ -316,22 +294,6 @@ class _RelayListItem extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - // Test button - OutlinedButton.icon( - onPressed: isTesting ? null : onTest, - icon: isTesting - ? const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.network_check, size: 16), - label: const Text('Test'), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - ), - ), - const SizedBox(width: 8), // Toggle switch Row( children: [ diff --git a/lib/ui/session/session_screen.dart b/lib/ui/session/session_screen.dart index 12d6678..c179def 100644 --- a/lib/ui/session/session_screen.dart +++ b/lib/ui/session/session_screen.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import '../../core/logger.dart'; import '../../core/service_locator.dart'; -import '../../data/session/session_service.dart'; -import '../../data/firebase/firebase_service.dart'; import '../../data/nostr/nostr_service.dart'; import '../../data/nostr/models/nostr_keypair.dart'; diff --git a/lib/ui/settings/settings_screen.dart b/lib/ui/settings/settings_screen.dart index 5667fa4..ad60b24 100644 --- a/lib/ui/settings/settings_screen.dart +++ b/lib/ui/settings/settings_screen.dart @@ -1,8 +1,5 @@ import 'package:flutter/material.dart'; import '../../core/service_locator.dart'; -import '../../data/firebase/firebase_service.dart'; -import '../../data/nostr/nostr_service.dart'; -import '../../data/sync/sync_engine.dart'; import '../relay_management/relay_management_screen.dart'; import '../relay_management/relay_management_controller.dart';