import 'package:flutter/material.dart'; import '../../data/nostr/nostr_service.dart'; import '../../data/nostr/models/nostr_keypair.dart'; import '../../data/nostr/models/nostr_event.dart'; import '../../data/nostr/models/nostr_relay.dart'; import '../../data/sync/sync_engine.dart'; /// Screen for displaying and testing Nostr events. class NostrEventsScreen extends StatefulWidget { final NostrService? nostrService; final SyncEngine? syncEngine; const NostrEventsScreen({ super.key, this.nostrService, this.syncEngine, }); @override State createState() => _NostrEventsScreenState(); } class _NostrEventsScreenState extends State { NostrKeyPair? _keyPair; List _relays = []; Map _connectionStatus = {}; List _events = []; bool _isLoading = false; final TextEditingController _nsecController = TextEditingController(); final TextEditingController _npubController = TextEditingController(); final TextEditingController _hexPrivateKeyController = TextEditingController(); bool _showImportFields = false; @override void initState() { super.initState(); _loadRelays(); _generateKeyPair(); } @override void dispose() { _nsecController.dispose(); _npubController.dispose(); _hexPrivateKeyController.dispose(); super.dispose(); } void _loadRelays() { if (widget.nostrService == null) return; setState(() { _relays = widget.nostrService!.getRelays(); _connectionStatus = { for (var relay in _relays) relay.url: relay.isConnected }; }); } void _generateKeyPair() { if (widget.nostrService == null) return; setState(() { _keyPair = widget.nostrService!.generateKeyPair(); _nsecController.clear(); _npubController.clear(); _hexPrivateKeyController.clear(); _showImportFields = false; }); } void _importFromNsec() { final nsec = _nsecController.text.trim(); if (nsec.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please enter an nsec key'), backgroundColor: Colors.orange, ), ); return; } try { setState(() { _keyPair = NostrKeyPair.fromNsec(nsec); _showImportFields = false; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Keypair imported from nsec'), backgroundColor: Colors.green, ), ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to import nsec: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } void _importFromNpub() { final npub = _npubController.text.trim(); if (npub.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please enter an npub key'), backgroundColor: Colors.orange, ), ); return; } try { setState(() { _keyPair = NostrKeyPair.fromNpub(npub); _showImportFields = false; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Public key imported from npub (note: cannot sign events without private key)'), backgroundColor: Colors.orange, ), ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to import npub: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } void _importFromHexPrivateKey() { final hexKey = _hexPrivateKeyController.text.trim(); if (hexKey.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please enter a hex private key'), backgroundColor: Colors.orange, ), ); return; } try { setState(() { _keyPair = NostrKeyPair.fromHexPrivateKey(hexKey); _showImportFields = false; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Keypair imported from hex private key'), backgroundColor: Colors.green, ), ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to import hex key: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } Future _connectToRelay(String relayUrl) async { if (widget.nostrService == null) return; setState(() { _isLoading = true; }); try { await widget.nostrService!.connectRelay(relayUrl).timeout( const Duration(seconds: 5), onTimeout: () { throw Exception('Connection timeout'); }, ); setState(() { _connectionStatus[relayUrl] = true; _isLoading = false; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Connected to $relayUrl'), backgroundColor: Colors.green, ), ); } _loadRelays(); } catch (e) { setState(() { _connectionStatus[relayUrl] = false; _isLoading = false; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to connect: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } } Future _disconnectFromRelay(String relayUrl) async { if (widget.nostrService == null) return; widget.nostrService!.disconnectRelay(relayUrl); setState(() { _connectionStatus[relayUrl] = false; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Disconnected from $relayUrl'), ), ); } _loadRelays(); } Future _publishTestEvent() async { if (widget.nostrService == null || _keyPair == null) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Nostr service or keypair not available'), backgroundColor: Colors.red, ), ); } return; } if (_relays.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('No relays configured. Add relays in Settings.'), backgroundColor: Colors.orange, ), ); } return; } setState(() { _isLoading = true; }); if (_keyPair!.privateKey.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Cannot publish: No private key available. Import nsec or hex private key.'), backgroundColor: Colors.red, ), ); } setState(() { _isLoading = false; }); return; } try { // Create a test event final event = NostrEvent.create( content: 'Test event from Flutter app - ${DateTime.now().toIso8601String()}', kind: 1, // Text note privateKey: _keyPair!.privateKey, ); // Publish to all connected relays final results = await widget.nostrService!.publishEventToAllRelays(event); setState(() { _isLoading = false; _events.insert(0, 'Event published: ${event.id.substring(0, 8)}...'); }); final successCount = results.values.where((v) => v == true).length; final totalCount = results.length; if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Published to $successCount/$totalCount relays'), backgroundColor: successCount > 0 ? Colors.green : Colors.orange, ), ); } } catch (e) { setState(() { _isLoading = false; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to publish: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Nostr Events'), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: () { _loadRelays(); _generateKeyPair(); }, tooltip: 'Refresh', ), ], ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildKeyPairSection(), const SizedBox(height: 24), _buildRelaysSection(), const SizedBox(height: 24), _buildActionsSection(), if (_events.isNotEmpty) ...[ const SizedBox(height: 24), _buildEventsSection(), ], ], ), ), ); } Widget _buildKeyPairSection() { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Keypair', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), TextButton.icon( onPressed: () { setState(() { _showImportFields = !_showImportFields; }); }, icon: Icon(_showImportFields ? Icons.visibility_off : Icons.import_export), label: Text(_showImportFields ? 'Hide Import' : 'Import Key'), ), ], ), const SizedBox(height: 12), if (_keyPair != null) ...[ const Text( 'Public Key:', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold), ), SelectableText( _keyPair!.publicKey, style: const TextStyle(fontSize: 11, fontFamily: 'monospace'), ), const SizedBox(height: 8), if (_keyPair!.privateKey.isNotEmpty) ...[ const Text( 'Private Key:', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold), ), SelectableText( _keyPair!.privateKey, style: const TextStyle(fontSize: 11, fontFamily: 'monospace'), ), ] else ...[ const Text( 'Private Key: (not available - imported from npub only)', style: TextStyle(fontSize: 12, color: Colors.orange), ), ], ] else ...[ const Text('No keypair generated'), ], const SizedBox(height: 12), if (_showImportFields) ...[ const Divider(), const SizedBox(height: 8), const Text( 'Import Key', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), TextField( controller: _nsecController, decoration: const InputDecoration( labelText: 'nsec (private key)', hintText: 'nsec1...', border: OutlineInputBorder(), helperText: 'Enter your nsec private key', ), ), const SizedBox(height: 8), ElevatedButton( onPressed: _importFromNsec, child: const Text('Import from nsec'), ), const SizedBox(height: 16), TextField( controller: _npubController, decoration: const InputDecoration( labelText: 'npub (public key)', hintText: 'npub1...', border: OutlineInputBorder(), helperText: 'Enter npub to view only (cannot sign events)', ), ), const SizedBox(height: 8), ElevatedButton( onPressed: _importFromNpub, child: const Text('Import from npub'), ), const SizedBox(height: 16), TextField( controller: _hexPrivateKeyController, decoration: const InputDecoration( labelText: 'Hex Private Key', hintText: '64 hex characters', border: OutlineInputBorder(), helperText: 'Enter private key in hex format (64 characters)', ), ), const SizedBox(height: 8), ElevatedButton( onPressed: _importFromHexPrivateKey, child: const Text('Import from Hex'), ), const Divider(), const SizedBox(height: 8), ], ElevatedButton.icon( onPressed: _generateKeyPair, icon: const Icon(Icons.refresh), label: const Text('Generate New Keypair'), ), ], ), ), ); } Widget _buildRelaysSection() { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Relays', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 12), if (_relays.isEmpty) const Text( 'No relays configured.\nAdd relays in Settings → Relay Management.', style: TextStyle(color: Colors.grey), ) else ..._relays.map((relay) => _buildRelayItem(relay)), ], ), ), ); } Widget _buildRelayItem(NostrRelay relay) { final isConnected = _connectionStatus[relay.url] ?? false; return Card( margin: const EdgeInsets.only(bottom: 8), color: isConnected ? Colors.green.shade50 : Colors.grey.shade50, child: ListTile( title: Text( relay.url, style: const TextStyle(fontSize: 14, fontFamily: 'monospace'), ), subtitle: Text( isConnected ? 'Connected' : 'Disconnected', style: TextStyle( color: isConnected ? Colors.green : Colors.grey, ), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( isConnected ? Icons.check_circle : Icons.cancel, color: isConnected ? Colors.green : Colors.grey, ), const SizedBox(width: 8), if (isConnected) IconButton( icon: const Icon(Icons.close, size: 20), onPressed: () => _disconnectFromRelay(relay.url), tooltip: 'Disconnect', ) else IconButton( icon: const Icon(Icons.play_arrow, size: 20), onPressed: () => _connectToRelay(relay.url), tooltip: 'Connect', ), ], ), ), ); } Widget _buildActionsSection() { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( 'Actions', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 12), ElevatedButton.icon( onPressed: _publishTestEvent, icon: const Icon(Icons.send), label: const Text('Publish Test Event'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), ), ), const SizedBox(height: 8), TextButton.icon( onPressed: () { // Navigate to Settings where Relay Management is accessible // The bottom navigation bar will handle switching to Settings tab ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Go to Settings tab to manage relays'), ), ); }, icon: const Icon(Icons.settings), label: const Text('Manage Relays'), ), ], ), ), ); } Widget _buildEventsSection() { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Recent Events', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 12), ..._events.take(5).map((event) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( event, style: const TextStyle(fontSize: 12), ), )), ], ), ), ); } }