nostr login fetch details and nip05
This commit is contained in:
@@ -131,6 +131,7 @@ class AppRouter {
|
||||
builder: (_) => SessionScreen(
|
||||
sessionService: sessionService,
|
||||
firebaseService: firebaseService,
|
||||
nostrService: nostrService,
|
||||
),
|
||||
settings: settings,
|
||||
);
|
||||
|
||||
@@ -92,6 +92,7 @@ class _MainNavigationScaffoldState extends State<MainNavigationScaffold> {
|
||||
return SessionScreen(
|
||||
sessionService: widget.sessionService,
|
||||
firebaseService: widget.firebaseService,
|
||||
nostrService: widget.nostrService,
|
||||
onSessionChanged: _onSessionStateChanged,
|
||||
);
|
||||
case 4:
|
||||
|
||||
@@ -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 {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../data/session/session_service.dart';
|
||||
import '../../data/firebase/firebase_service.dart';
|
||||
import '../../data/nostr/nostr_service.dart';
|
||||
|
||||
/// Screen for user session management (login/logout).
|
||||
class SessionScreen extends StatefulWidget {
|
||||
final SessionService? sessionService;
|
||||
final FirebaseService? firebaseService;
|
||||
final NostrService? nostrService;
|
||||
final VoidCallback? onSessionChanged;
|
||||
|
||||
const SessionScreen({
|
||||
super.key,
|
||||
this.sessionService,
|
||||
this.firebaseService,
|
||||
this.nostrService,
|
||||
this.onSessionChanged,
|
||||
});
|
||||
|
||||
@@ -342,6 +345,17 @@ class _SessionScreenState extends State<SessionScreen> {
|
||||
const Divider(),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
// NIP-05 section
|
||||
if (currentUser.nostrProfile?.nip05 != null &&
|
||||
currentUser.nostrProfile!.nip05!.isNotEmpty)
|
||||
_Nip05Section(
|
||||
nip05: currentUser.nostrProfile!.nip05!,
|
||||
publicKey: currentUser.id,
|
||||
nostrService: widget.nostrService,
|
||||
),
|
||||
if (currentUser.nostrProfile?.nip05 != null &&
|
||||
currentUser.nostrProfile!.nip05!.isNotEmpty)
|
||||
const SizedBox(height: 12),
|
||||
Text('User ID: ${currentUser.id.substring(0, currentUser.id.length > 32 ? 32 : currentUser.id.length)}${currentUser.id.length > 32 ? '...' : ''}'),
|
||||
Text('Username: ${currentUser.username}'),
|
||||
Text(
|
||||
@@ -494,3 +508,183 @@ class _SessionScreenState extends State<SessionScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget for displaying NIP-05 information including domain and preferred relays.
|
||||
class _Nip05Section extends StatefulWidget {
|
||||
final String nip05;
|
||||
final String publicKey;
|
||||
final NostrService? nostrService;
|
||||
|
||||
const _Nip05Section({
|
||||
required this.nip05,
|
||||
required this.publicKey,
|
||||
this.nostrService,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_Nip05Section> createState() => _Nip05SectionState();
|
||||
}
|
||||
|
||||
class _Nip05SectionState extends State<_Nip05Section> {
|
||||
List<String> _preferredRelays = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadPreferredRelays();
|
||||
}
|
||||
|
||||
Future<void> _loadPreferredRelays() async {
|
||||
if (widget.nostrService == null) {
|
||||
setState(() {
|
||||
_error = 'Nostr service not available';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final relays = await widget.nostrService!.fetchPreferredRelaysFromNip05(
|
||||
widget.nip05,
|
||||
widget.publicKey,
|
||||
);
|
||||
setState(() {
|
||||
_preferredRelays = relays;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = e.toString().replaceAll('NostrException: ', '');
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String _getDomain() {
|
||||
final parts = widget.nip05.split('@');
|
||||
if (parts.length == 2) {
|
||||
return parts[1];
|
||||
}
|
||||
return widget.nip05;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'NIP-05',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.verified, size: 16, color: Colors.blue),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.nip05,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Domain: ${_getDomain()}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Preferred Relays',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (_isLoading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
)
|
||||
else if (_error != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 16, color: Colors.red.shade700),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_error!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.red.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else if (_preferredRelays.isEmpty)
|
||||
Text(
|
||||
'No preferred relays found',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
)
|
||||
else
|
||||
..._preferredRelays.map((relay) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.link,
|
||||
size: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
relay,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user