nostr login fetch details and nip05

This commit is contained in:
gitea
2025-11-06 21:07:14 +01:00
parent d68124d975
commit 120a04c69f
10 changed files with 652 additions and 118 deletions
@@ -118,6 +118,68 @@ class RelayManagementController extends ChangeNotifier {
}
}
/// Tests connectivity to a single relay.
///
/// [relayUrl] - The URL of the relay to test.
///
/// Returns true if connection successful, false otherwise.
Future<bool> testRelay(String relayUrl) async {
_error = null;
notifyListeners();
try {
final stream = await nostrService
.connectRelay(relayUrl)
.timeout(
const Duration(seconds: 3),
onTimeout: () {
throw Exception('Connection timeout');
},
);
_loadRelays();
// Cancel the stream subscription to clean up
stream.listen(null).cancel();
return true;
} catch (e) {
// Connection failed - disconnect to mark as unhealthy
try {
nostrService.disconnectRelay(relayUrl);
_loadRelays();
} catch (_) {
// Ignore disconnect errors
}
return false;
}
}
/// Toggles a relay on/off (enables/disables it).
///
/// [relayUrl] - The URL of the relay to toggle.
void toggleRelay(String relayUrl) {
try {
_error = null;
final relay = _relays.firstWhere((r) => r.url == relayUrl);
nostrService.setRelayEnabled(relayUrl, !relay.isEnabled);
_loadRelays();
} catch (e) {
_error = 'Failed to toggle relay: $e';
notifyListeners();
}
}
/// Toggles all relays on/off.
void toggleAllRelays() {
try {
_error = null;
final allEnabled = _relays.every((r) => r.isEnabled);
nostrService.setAllRelaysEnabled(!allEnabled);
_loadRelays();
} catch (e) {
_error = 'Failed to toggle all relays: $e';
notifyListeners();
}
}
/// Checks health of all relays by attempting to connect.
///
/// Updates relay connection status.
@@ -4,8 +4,7 @@ import '../../data/nostr/models/nostr_relay.dart';
/// Screen for managing Nostr relays.
///
/// Allows users to view, add, remove, and monitor relay health,
/// and trigger manual syncs.
/// Allows users to view, add, remove, test, and toggle relays.
class RelayManagementScreen extends StatefulWidget {
/// Controller for managing relay state.
final RelayManagementController controller;
@@ -22,6 +21,7 @@ class RelayManagementScreen extends StatefulWidget {
class _RelayManagementScreenState extends State<RelayManagementScreen> {
final TextEditingController _urlController = TextEditingController();
final Map<String, bool> _testingRelays = {};
@override
void dispose() {
@@ -29,6 +29,31 @@ class _RelayManagementScreenState extends State<RelayManagementScreen> {
super.dispose();
}
Future<void> _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(
@@ -65,12 +90,48 @@ class _RelayManagementScreenState extends State<RelayManagementScreen> {
),
),
// Actions section
// Top action buttons
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Test All and Toggle All buttons
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: widget.controller.isCheckingHealth
? null
: widget.controller.checkRelayHealth,
icon: widget.controller.isCheckingHealth
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.network_check),
label: const Text('Test All'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: widget.controller.relays.isEmpty
? null
: widget.controller.toggleAllRelays,
icon: const Icon(Icons.power_settings_new),
label: Text(
widget.controller.relays.isNotEmpty &&
widget.controller.relays.every((r) => r.isEnabled)
? 'Turn All Off'
: 'Turn All On',
),
),
),
],
),
const SizedBox(height: 16),
// Add relay input
Row(
children: [
@@ -79,9 +140,7 @@ class _RelayManagementScreenState extends State<RelayManagementScreen> {
controller: _urlController,
decoration: InputDecoration(
labelText: 'Relay URL',
hintText: widget.controller.relays.isNotEmpty
? widget.controller.relays.first.url
: 'wss://nostrum.satoshinakamoto.win',
hintText: 'wss://relay.example.com',
border: const OutlineInputBorder(),
),
keyboardType: TextInputType.url,
@@ -108,56 +167,6 @@ class _RelayManagementScreenState extends State<RelayManagementScreen> {
),
],
),
const SizedBox(height: 16),
// Action buttons
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: widget.controller.isCheckingHealth
? null
: widget.controller.checkRelayHealth,
icon: widget.controller.isCheckingHealth
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.health_and_safety),
label: const Text('Check Health'),
),
if (widget.controller.syncEngine != null)
ElevatedButton.icon(
onPressed: widget.controller.isSyncing
? null
: () async {
final success = await widget.controller.triggerManualSync();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
success
? 'Sync triggered successfully'
: 'Sync failed: ${widget.controller.error ?? "Unknown error"}',
),
duration: const Duration(seconds: 2),
),
);
}
},
icon: widget.controller.isSyncing
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.sync),
label: const Text('Manual Sync'),
),
],
),
],
),
),
@@ -199,8 +208,9 @@ class _RelayManagementScreenState extends State<RelayManagementScreen> {
final relay = widget.controller.relays[index];
return _RelayListItem(
relay: relay,
onConnect: () => widget.controller.connectRelay(relay.url),
onDisconnect: () => widget.controller.disconnectRelay(relay.url),
isTesting: _testingRelays[relay.url] ?? false,
onTest: () => _handleTestRelay(relay.url),
onToggle: () => widget.controller.toggleRelay(relay.url),
onRemove: () {
widget.controller.removeRelay(relay.url);
ScaffoldMessenger.of(context).showSnackBar(
@@ -227,19 +237,23 @@ class _RelayListItem extends StatelessWidget {
/// The relay to display.
final NostrRelay relay;
/// Callback when connect is pressed.
final VoidCallback onConnect;
/// Whether the relay is currently being tested.
final bool isTesting;
/// Callback when disconnect is pressed.
final VoidCallback onDisconnect;
/// Callback when test is pressed.
final VoidCallback onTest;
/// Callback when toggle is pressed.
final VoidCallback onToggle;
/// Callback when remove is pressed.
final VoidCallback onRemove;
const _RelayListItem({
required this.relay,
required this.onConnect,
required this.onDisconnect,
required this.isTesting,
required this.onTest,
required this.onToggle,
required this.onRemove,
});
@@ -247,40 +261,103 @@ class _RelayListItem extends StatelessWidget {
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: relay.isConnected ? Colors.green : Colors.grey,
child: Icon(
relay.isConnected ? Icons.check : Icons.close,
color: Colors.white,
size: 20,
),
),
title: Text(
relay.url,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
relay.isConnected ? 'Connected' : 'Disconnected',
style: TextStyle(
color: relay.isConnected ? Colors.green : Colors.grey,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconButton(
icon: Icon(
relay.isConnected ? Icons.link_off : Icons.link,
color: relay.isConnected ? Colors.orange : Colors.green,
),
tooltip: relay.isConnected ? 'Disconnect' : 'Connect',
onPressed: relay.isConnected ? onDisconnect : onConnect,
// Relay URL and status
Row(
children: [
// Status indicator
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: relay.isConnected
? Colors.green
: relay.isEnabled
? Colors.orange
: Colors.grey,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
relay.url,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
],
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
tooltip: 'Remove',
onPressed: onRemove,
const SizedBox(height: 8),
// Status text
Text(
relay.isConnected
? 'Connected'
: relay.isEnabled
? 'Enabled (not connected)'
: 'Disabled',
style: TextStyle(
fontSize: 12,
color: relay.isConnected
? Colors.green
: relay.isEnabled
? Colors.orange
: Colors.grey,
),
),
const SizedBox(height: 12),
// Action buttons
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: [
Text(
relay.isEnabled ? 'On' : 'Off',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
const SizedBox(width: 4),
Switch(
value: relay.isEnabled,
onChanged: (_) => onToggle(),
),
],
),
const SizedBox(width: 8),
// Remove button
IconButton(
icon: const Icon(Icons.delete, size: 20),
color: Colors.red,
tooltip: 'Remove',
onPressed: onRemove,
),
],
),
],
),
@@ -288,4 +365,3 @@ class _RelayListItem extends StatelessWidget {
);
}
}