From d8c90cb105d7595562db463d825eb0413b62b30e Mon Sep 17 00:00:00 2001 From: gitea Date: Thu, 6 Nov 2025 00:55:45 +0100 Subject: [PATCH] nostr v1 --- lib/ui/nostr_events/nostr_events_screen.dart | 399 ++++++++++++++++++- 1 file changed, 377 insertions(+), 22 deletions(-) diff --git a/lib/ui/nostr_events/nostr_events_screen.dart b/lib/ui/nostr_events/nostr_events_screen.dart index a0c73cc..e9ae1fc 100644 --- a/lib/ui/nostr_events/nostr_events_screen.dart +++ b/lib/ui/nostr_events/nostr_events_screen.dart @@ -1,9 +1,12 @@ 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 Nostr events (placeholder). -class NostrEventsScreen extends StatelessWidget { +/// Screen for displaying and testing Nostr events. +class NostrEventsScreen extends StatefulWidget { final NostrService? nostrService; final SyncEngine? syncEngine; @@ -13,45 +16,396 @@ class NostrEventsScreen extends StatelessWidget { this.syncEngine, }); + @override + State createState() => _NostrEventsScreenState(); +} + +class _NostrEventsScreenState extends State { + NostrKeyPair? _keyPair; + List _relays = []; + Map _connectionStatus = {}; + List _events = []; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _loadRelays(); + _generateKeyPair(); + } + + 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(); + }); + } + + 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; + }); + + 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: const Center( + 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( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Icons.cloud_outlined, - size: 64, - color: Colors.grey, + const Text( + 'Keypair', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + if (_keyPair != null) ...[ + Text( + 'Public Key: ${_keyPair!.publicKey.substring(0, 16)}...', + style: const TextStyle(fontSize: 12, fontFamily: 'monospace'), + ), + const SizedBox(height: 4), + Text( + 'Private Key: ${_keyPair!.privateKey.substring(0, 16)}...', + style: const TextStyle(fontSize: 12, fontFamily: 'monospace'), + ), + ] else ...[ + const Text('No keypair generated'), + ], + const SizedBox(height: 12), + ElevatedButton.icon( + onPressed: _generateKeyPair, + icon: const Icon(Icons.refresh), + label: const Text('Generate New Keypair'), ), - SizedBox(height: 16), - Text( - 'Nostr Events', + ], + ), + ), + ); + } + + Widget _buildRelaysSection() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Relays', style: TextStyle( - fontSize: 20, + fontSize: 18, fontWeight: FontWeight.bold, ), ), - SizedBox(height: 8), - Text( - 'This screen will display Nostr events', + 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: 14, - color: Colors.grey, + fontSize: 18, + fontWeight: FontWeight.bold, ), ), - SizedBox(height: 24), - Text( - 'Placeholder: Add your Nostr events UI here', + 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: 12, - color: Colors.grey, + 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), + ), + )), ], ), ), @@ -59,3 +413,4 @@ class NostrEventsScreen extends StatelessWidget { } } +