From 23b49d36ff44185fba12735eb64a532b8653737d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 26 Sep 2023 21:40:18 +0200 Subject: [PATCH 1/8] current stand --- lib/utils/nostr_fetcher/NostrSocket.dart | 3 ++- lib/utils/nostr_fetcher/Socket.dart | 6 ++++++ lib/widgets/RelaySelectSheet.dart | 27 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/utils/nostr_fetcher/NostrSocket.dart b/lib/utils/nostr_fetcher/NostrSocket.dart index 2d4ae28d..d8e77b3f 100644 --- a/lib/utils/nostr_fetcher/NostrSocket.dart +++ b/lib/utils/nostr_fetcher/NostrSocket.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter_logs/flutter_logs.dart'; import 'package:locus/constants/values.dart'; import 'package:locus/services/location_point_service.dart'; -import 'package:locus/services/task_service/index.dart'; import 'package:locus/services/task_service/mixins.dart'; import 'package:locus/utils/nostr_fetcher/BasicNostrFetchSocket.dart'; import 'package:locus/utils/nostr_fetcher/Socket.dart'; @@ -121,10 +120,12 @@ class NostrSocket extends BasicNostrFetchSocket { final int? limit, final DateTime? from, final DateTime? until, + final List? authors, }) => Filter( kinds: kinds, limit: limit, + authors: authors ?? [], since: from == null ? null : (from.millisecondsSinceEpoch / 1000).floor(), until: until == null diff --git a/lib/utils/nostr_fetcher/Socket.dart b/lib/utils/nostr_fetcher/Socket.dart index f22b8938..78c4f994 100644 --- a/lib/utils/nostr_fetcher/Socket.dart +++ b/lib/utils/nostr_fetcher/Socket.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:flutter_logs/flutter_logs.dart'; @@ -75,6 +76,11 @@ abstract class Socket { return; } + // Prettify event.content + print( + jsonDecode(event)[2], + ); + _resetTimer(); onEvent(event); diff --git a/lib/widgets/RelaySelectSheet.dart b/lib/widgets/RelaySelectSheet.dart index 6ca5108f..f4bfebd1 100644 --- a/lib/widgets/RelaySelectSheet.dart +++ b/lib/widgets/RelaySelectSheet.dart @@ -5,12 +5,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_logs/flutter_logs.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:latlong2/latlong.dart'; import 'package:locus/constants/spacing.dart'; import 'package:locus/constants/values.dart'; +import 'package:locus/services/location_point_service.dart'; import 'package:locus/utils/load_status.dart'; +import 'package:locus/utils/nostr_fetcher/NostrSocket.dart'; import 'package:locus/utils/theme.dart'; import 'package:locus/widgets/BottomSheetFilterBuilder.dart'; import 'package:locus/widgets/ModalSheet.dart'; +import 'package:nostr/nostr.dart'; import '../api/nostr-relays.dart'; import '../utils/cache.dart'; @@ -135,6 +139,29 @@ class _RelaySelectSheetState extends State { _closeSheet(); } }); + + final socket = NostrSocket( + relay: "wss://history.nostr.watch", + decryptMessage: (_) async { + return LocationPointService.dummyFromLatLng(LatLng(0, 0)); + }, + ); + socket.connect().then((_) { + socket.addData( + Request( + generate64RandomHexChars(), + [ + NostrSocket.createNostrRequestData( + kinds: [30304], + limit: 10, + authors: [ + "b3b0d247f66bf40c4c9f4ce721abfe1fd3b7529fbc1ea5e64d5f0f8df3a4b6e6" + ], + ), + ], + ).serialize(), + ); + }); } _closeSheet() { From 64c874c710c621dda9a28245069c81b5f9a220bd Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 26 Sep 2023 23:38:11 +0200 Subject: [PATCH 2/8] feat: Add get-relays-meta.dart functionality --- lib/api/get-relays-meta.dart | 175 +++++++++++++++++++++++ lib/main.dart | 1 + lib/screens/LocationsOverviewScreen.dart | 3 + lib/utils/access-deeply-nested-key.dart | 15 ++ lib/utils/nostr_fetcher/Socket.dart | 5 - lib/widgets/RelaySelectSheet.dart | 23 --- 6 files changed, 194 insertions(+), 28 deletions(-) create mode 100644 lib/api/get-relays-meta.dart create mode 100644 lib/utils/access-deeply-nested-key.dart diff --git a/lib/api/get-relays-meta.dart b/lib/api/get-relays-meta.dart new file mode 100644 index 00000000..27cdf3de --- /dev/null +++ b/lib/api/get-relays-meta.dart @@ -0,0 +1,175 @@ +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:locus/utils/access-deeply-nested-key.dart'; +import 'package:locus/utils/nostr_fetcher/BasicNostrFetchSocket.dart'; +import 'package:locus/utils/nostr_fetcher/NostrSocket.dart'; +import 'package:nostr/nostr.dart'; + +const MIN_LENGTH = 5000; + +class RelaysMetaFetcher extends BasicNostrFetchSocket { + List meta = []; + + RelaysMetaFetcher({ + required super.relay, + super.timeout, + }); + + @override + void onEndOfStream() { + closeConnection(); + } + + @override + void onNostrEvent(final Message message) { + // Relay URL, canWrite and canRead are in message.tags + // Latencies are saved in content, separated per region + // with the following schema: + // [ + // [], + // [], + // [], + // ] + final event = message.message as Event; + + final relayMeta = RelayMeta.fromFetchedContent( + canWrite: event.tags[1][1] == "true", + canRead: event.tags[2][1] == "true", + relay: event.tags[0][1], + content: jsonDecode(event.content), + worldRegion: "eu-west", + ); + + meta.add(relayMeta); + } + + @override + void onError(error) { + closeConnection(); + } +} + +class RelayMeta { + final String relay; + final bool canWrite; + final bool canRead; + final String contactInfo; + final String description; + final String name; + + final List connectionLatencies; + final List readLatencies; + final List writeLatencies; + + final int maxMessageLength; + final int maxContentLength; + + final int minPowDifficulty; + final bool requiresPayment; + + const RelayMeta({ + required this.relay, + required this.canWrite, + required this.canRead, + required this.contactInfo, + required this.description, + required this.name, + required this.connectionLatencies, + required this.readLatencies, + required this.writeLatencies, + required this.maxMessageLength, + required this.maxContentLength, + required this.minPowDifficulty, + required this.requiresPayment, + }); + + factory RelayMeta.fromFetchedContent({ + required final Map content, + required final String relay, + required final bool canRead, + required final bool canWrite, + required final String worldRegion, + }) => + RelayMeta( + relay: relay, + canRead: canRead, + canWrite: canWrite, + name: adnk(content, "info.name") ?? relay, + contactInfo: adnk(content, "info.contact") ?? "", + description: adnk(content, "info.description") ?? "", + connectionLatencies: + List.from(adnk(content, "latency$worldRegion.0") ?? []) + .where((value) => value != null) + .toList() + .cast(), + readLatencies: + List.from(adnk(content, "latency$worldRegion.1") ?? []) + .where((value) => value != null) + .toList() + .cast(), + writeLatencies: + List.from(adnk(content, "latency$worldRegion.2") ?? []) + .where((value) => value != null) + .toList() + .cast(), + maxContentLength: + adnk(content, "info.limitations.max_content_length") ?? 0, + maxMessageLength: + adnk(content, "info.limitations.max_message_length") ?? 0, + requiresPayment: + adnk(content, "info.limitations.payment_required") ?? false, + minPowDifficulty: + adnk(content, "info.limitations.min_pow_difficulty") ?? 0); + + bool get isSuitable => + canWrite && + canRead && + !requiresPayment && + minPowDifficulty == 0 && + maxContentLength >= MIN_LENGTH; + + // Calculate average latency, we use the average as we want extreme highs + // to be taken into account. + double get score { + if (connectionLatencies.isEmpty || + readLatencies.isEmpty || + writeLatencies.isEmpty) { + // If there is no data available, we don't know if the relay is fully intact + return double.infinity; + } + + // Each latency has it's own factor to give each of them a different weight + // Lower latency = better - Because of this + // a factor closer to 0 resembles a HIGHER weight + // We prioritize read latency as we want to be able to provide a fast app + return (connectionLatencies.average * 0.9 + + readLatencies.average * 0.5 + + writeLatencies.average) + + (maxContentLength - MIN_LENGTH) * 0.0001; + } +} + +final REQUEST_DATA = NostrSocket.createNostrRequestData( + kinds: [30304], + limit: 10, + authors: ["b3b0d247f66bf40c4c9f4ce721abfe1fd3b7529fbc1ea5e64d5f0f8df3a4b6e6"], +); + +Future fetchRelaysMeta() async { + final fetcher = RelaysMetaFetcher( + relay: "wss://history.nostr.watch", + ); + await fetcher.connect(); + fetcher.addData( + Request( + generate64RandomHexChars(), + [ + REQUEST_DATA, + ], + ).serialize(), + ); + await fetcher.onComplete; + + print(fetcher.meta); +} diff --git a/lib/main.dart b/lib/main.dart index cd77f55c..cb007505 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_logs/flutter_logs.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:locus/App.dart'; +import 'package:locus/api/get-relays-meta.dart'; import 'package:locus/screens/locations_overview_screen_widgets/LocationFetchers.dart'; import 'package:locus/services/app_update_service.dart'; import 'package:locus/services/current_location_service.dart'; diff --git a/lib/screens/LocationsOverviewScreen.dart b/lib/screens/LocationsOverviewScreen.dart index 48b07cfb..03805150 100644 --- a/lib/screens/LocationsOverviewScreen.dart +++ b/lib/screens/LocationsOverviewScreen.dart @@ -19,6 +19,7 @@ import 'package:flutter_map_marker_popup/flutter_map_marker_popup.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:geolocator/geolocator.dart'; import 'package:latlong2/latlong.dart'; +import 'package:locus/api/get-relays-meta.dart'; import 'package:locus/constants/spacing.dart'; import 'package:locus/screens/ImportTaskSheet.dart'; import 'package:locus/screens/SettingsScreen.dart'; @@ -194,6 +195,8 @@ class _LocationsOverviewScreenState extends State flutterMapPopupController = PopupController(); } + + fetchRelaysMeta(); } @override diff --git a/lib/utils/access-deeply-nested-key.dart b/lib/utils/access-deeply-nested-key.dart new file mode 100644 index 00000000..981e8830 --- /dev/null +++ b/lib/utils/access-deeply-nested-key.dart @@ -0,0 +1,15 @@ +T? accessDeeplyNestedKey(final Map obj, final String path) { + dynamic result = obj; + + for (final subPath in path.split(".")) { + if (result.containsKey(subPath)) { + result = result[subPath]; + } else { + return null; + } + } + + return result as T; +} + +const adnk = accessDeeplyNestedKey; diff --git a/lib/utils/nostr_fetcher/Socket.dart b/lib/utils/nostr_fetcher/Socket.dart index 78c4f994..b7674994 100644 --- a/lib/utils/nostr_fetcher/Socket.dart +++ b/lib/utils/nostr_fetcher/Socket.dart @@ -76,11 +76,6 @@ abstract class Socket { return; } - // Prettify event.content - print( - jsonDecode(event)[2], - ); - _resetTimer(); onEvent(event); diff --git a/lib/widgets/RelaySelectSheet.dart b/lib/widgets/RelaySelectSheet.dart index f4bfebd1..82c3f792 100644 --- a/lib/widgets/RelaySelectSheet.dart +++ b/lib/widgets/RelaySelectSheet.dart @@ -139,29 +139,6 @@ class _RelaySelectSheetState extends State { _closeSheet(); } }); - - final socket = NostrSocket( - relay: "wss://history.nostr.watch", - decryptMessage: (_) async { - return LocationPointService.dummyFromLatLng(LatLng(0, 0)); - }, - ); - socket.connect().then((_) { - socket.addData( - Request( - generate64RandomHexChars(), - [ - NostrSocket.createNostrRequestData( - kinds: [30304], - limit: 10, - authors: [ - "b3b0d247f66bf40c4c9f4ce721abfe1fd3b7529fbc1ea5e64d5f0f8df3a4b6e6" - ], - ), - ], - ).serialize(), - ); - }); } _closeSheet() { From c757d00698869d8de6aebf4d6825b88b7996d0ec Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 27 Sep 2023 17:46:33 +0200 Subject: [PATCH 3/8] current stand --- lib/api/get-relays-meta.dart | 23 +++++--- lib/screens/LocationsOverviewScreen.dart | 2 - lib/utils/access-deeply-nested-key.dart | 4 +- lib/widgets/RelaySelectSheet.dart | 72 +++++++++++++++++++++--- 4 files changed, 83 insertions(+), 18 deletions(-) diff --git a/lib/api/get-relays-meta.dart b/lib/api/get-relays-meta.dart index 27cdf3de..fe3f1f08 100644 --- a/lib/api/get-relays-meta.dart +++ b/lib/api/get-relays-meta.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:collection/collection.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:locus/utils/access-deeply-nested-key.dart'; import 'package:locus/utils/nostr_fetcher/BasicNostrFetchSocket.dart'; import 'package:locus/utils/nostr_fetcher/NostrSocket.dart'; @@ -99,24 +100,26 @@ class RelayMeta { contactInfo: adnk(content, "info.contact") ?? "", description: adnk(content, "info.description") ?? "", connectionLatencies: - List.from(adnk(content, "latency$worldRegion.0") ?? []) + List.from(adnk(content, "latency.$worldRegion.0") ?? []) .where((value) => value != null) .toList() .cast(), readLatencies: - List.from(adnk(content, "latency$worldRegion.1") ?? []) + List.from(adnk(content, "latency.$worldRegion.1") ?? []) .where((value) => value != null) .toList() .cast(), writeLatencies: - List.from(adnk(content, "latency$worldRegion.2") ?? []) + List.from(adnk(content, "latency.$worldRegion.2") ?? []) .where((value) => value != null) .toList() .cast(), maxContentLength: - adnk(content, "info.limitations.max_content_length") ?? 0, + adnk(content, "info.limitations.max_content_length") ?? + MIN_LENGTH, maxMessageLength: - adnk(content, "info.limitations.max_message_length") ?? 0, + adnk(content, "info.limitations.max_message_length") ?? + MIN_LENGTH, requiresPayment: adnk(content, "info.limitations.payment_required") ?? false, minPowDifficulty: @@ -150,13 +153,15 @@ class RelayMeta { } } +// Values taken from https://github.com/dskvr/nostr-watch/blob/develop/src/components/relays/jobs/LoadSeed.vue#L91 final REQUEST_DATA = NostrSocket.createNostrRequestData( kinds: [30304], - limit: 10, + limit: 1000, + from: DateTime.now().subtract(2.hours), authors: ["b3b0d247f66bf40c4c9f4ce721abfe1fd3b7529fbc1ea5e64d5f0f8df3a4b6e6"], ); -Future fetchRelaysMeta() async { +Future>> fetchRelaysMeta() async { final fetcher = RelaysMetaFetcher( relay: "wss://history.nostr.watch", ); @@ -171,5 +176,7 @@ Future fetchRelaysMeta() async { ); await fetcher.onComplete; - print(fetcher.meta); + return { + "meta": fetcher.meta, + }; } diff --git a/lib/screens/LocationsOverviewScreen.dart b/lib/screens/LocationsOverviewScreen.dart index 03805150..d41315ae 100644 --- a/lib/screens/LocationsOverviewScreen.dart +++ b/lib/screens/LocationsOverviewScreen.dart @@ -195,8 +195,6 @@ class _LocationsOverviewScreenState extends State flutterMapPopupController = PopupController(); } - - fetchRelaysMeta(); } @override diff --git a/lib/utils/access-deeply-nested-key.dart b/lib/utils/access-deeply-nested-key.dart index 981e8830..738b85e4 100644 --- a/lib/utils/access-deeply-nested-key.dart +++ b/lib/utils/access-deeply-nested-key.dart @@ -2,7 +2,9 @@ T? accessDeeplyNestedKey(final Map obj, final String path) { dynamic result = obj; for (final subPath in path.split(".")) { - if (result.containsKey(subPath)) { + if (result is List + ? result[int.parse(subPath)] + : result.containsKey(subPath)) { result = result[subPath]; } else { return null; diff --git a/lib/widgets/RelaySelectSheet.dart b/lib/widgets/RelaySelectSheet.dart index 82c3f792..5f474575 100644 --- a/lib/widgets/RelaySelectSheet.dart +++ b/lib/widgets/RelaySelectSheet.dart @@ -6,6 +6,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_logs/flutter_logs.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:latlong2/latlong.dart'; +import 'package:locus/api/get-relays-meta.dart'; import 'package:locus/constants/spacing.dart'; import 'package:locus/constants/values.dart'; import 'package:locus/services/location_point_service.dart'; @@ -73,8 +74,10 @@ class RelaySelectSheet extends StatefulWidget { } class _RelaySelectSheetState extends State { - List availableRelays = []; + final List availableRelays = []; + final Map relayMeta = {}; LoadStatus loadStatus = LoadStatus.loading; + final _searchController = TextEditingController(); late final DraggableScrollableController _sheetController; String _newValue = ''; @@ -87,7 +90,8 @@ class _RelaySelectSheetState extends State { @override void initState() { super.initState(); - fetchAvailableRelays(); + _fetchAvailableRelays(); + _fetchRelaysMeta(); widget.controller.addListener(rebuild); _searchController.addListener(() { @@ -164,7 +168,55 @@ class _RelaySelectSheetState extends State { setState(() {}); } - Future fetchAvailableRelays() async { + // Filters all relays whether they are suitable + void _filterRelaysFromMeta() { + if (relayMeta.isEmpty || availableRelays.isEmpty) { + return; + } + + final suitableRelays = relayMeta.values + .where((meta) => meta.isSuitable) + .map((meta) => meta.relay) + .toSet(); + + setState(() { + availableRelays.retainWhere(suitableRelays.contains); + availableRelays.sort( + (a, b) => relayMeta[a]!.score > relayMeta[b]!.score ? -1 : 1, + ); + }); + } + + Future _fetchRelaysMeta() async { + FlutterLogs.logInfo( + LOG_TAG, + "Relay Select Sheet", + "Fetching relays meta...", + ); + + try { + final relaysMetaDataRaw = + await withCache(fetchRelaysMeta, "relays-meta")(); + final relaysMetaData = relaysMetaDataRaw["meta"] as List; + final newRelays = Map.fromEntries( + relaysMetaData.map((meta) => MapEntry(meta.relay, meta)), + ); + + relayMeta.clear(); + relayMeta.addAll(newRelays); + _filterRelaysFromMeta(); + + setState(() {}); + } catch (error) { + FlutterLogs.logError( + LOG_TAG, + "Relay Select Sheet", + "Failed to fetch available relays: $error", + ); + } + } + + Future _fetchAvailableRelays() async { FlutterLogs.logInfo( LOG_TAG, "Relay Select Sheet", @@ -177,10 +229,13 @@ class _RelaySelectSheetState extends State { relays.shuffle(); - setState(() { - availableRelays = relays; - loadStatus = LoadStatus.success; - }); + availableRelays + ..clear() + ..addAll(relays); + loadStatus = LoadStatus.success; + _filterRelaysFromMeta(); + + setState(() {}); } catch (error) { FlutterLogs.logError( LOG_TAG, @@ -247,12 +302,14 @@ class _RelaySelectSheetState extends State { final index = isValueNew ? rawIndex - 1 : rawIndex; final relay = allRelays[index]; + final meta = relayMeta[relay]; return PlatformWidget( material: (context, _) => CheckboxListTile( title: Text( relay.length >= 6 ? relay.substring(6) : relay, ), + subtitle: meta == null ? null : Text(meta.score.toString()), value: widget.controller.relays.contains(relay), onChanged: (newValue) { if (newValue == null) { @@ -270,6 +327,7 @@ class _RelaySelectSheetState extends State { title: Text( relay.length >= 6 ? relay.substring(6) : relay, ), + subtitle: meta == null ? null : Text(meta.description), trailing: CupertinoSwitch( value: widget.controller.relays.contains(relay), onChanged: (newValue) { From c210024eead38e3906ac0196a6f4bd5dd38e8a42 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 27 Sep 2023 18:03:16 +0200 Subject: [PATCH 4/8] fix: Improve spacing for iOS --- .../locations_overview_screen_widgets/ActiveSharesSheet.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/locations_overview_screen_widgets/ActiveSharesSheet.dart b/lib/screens/locations_overview_screen_widgets/ActiveSharesSheet.dart index a5bcdbc8..b43590ed 100644 --- a/lib/screens/locations_overview_screen_widgets/ActiveSharesSheet.dart +++ b/lib/screens/locations_overview_screen_widgets/ActiveSharesSheet.dart @@ -460,7 +460,7 @@ class _ActiveSharesSheetState extends State child: SizedBox.square(), ), Expanded( - flex: 8, + flex: 6, child: Center( child: Text( l10n.locationsOverview_activeShares_amount( From 8de4b32dbdc3bb5fe1419912b47f442770157b0c Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 27 Sep 2023 22:29:14 +0200 Subject: [PATCH 5/8] current stand --- lib/api/get-relays-meta.dart | 30 +++++++++++++------------ lib/utils/access-deeply-nested-key.dart | 8 ++++--- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/lib/api/get-relays-meta.dart b/lib/api/get-relays-meta.dart index fe3f1f08..180255f0 100644 --- a/lib/api/get-relays-meta.dart +++ b/lib/api/get-relays-meta.dart @@ -96,34 +96,36 @@ class RelayMeta { relay: relay, canRead: canRead, canWrite: canWrite, - name: adnk(content, "info.name") ?? relay, - contactInfo: adnk(content, "info.contact") ?? "", - description: adnk(content, "info.description") ?? "", - connectionLatencies: - List.from(adnk(content, "latency.$worldRegion.0") ?? []) - .where((value) => value != null) - .toList() - .cast(), + name: adnk(content, "info.name") ?? relay, + contactInfo: adnk(content, "info.contact") ?? "", + description: adnk(content, "info.description") ?? "", + connectionLatencies: List.from( + adnk(content, "latency.$worldRegion.0") ?? []) + .where((value) => value != null) + .toList() + .cast(), readLatencies: - List.from(adnk(content, "latency.$worldRegion.1") ?? []) + List.from(adnk(content, "latency.$worldRegion.1") ?? []) .where((value) => value != null) .toList() .cast(), writeLatencies: - List.from(adnk(content, "latency.$worldRegion.2") ?? []) + List.from(adnk(content, "latency.$worldRegion.2") ?? []) .where((value) => value != null) .toList() .cast(), maxContentLength: - adnk(content, "info.limitations.max_content_length") ?? + adnk(content, "info.limitations.max_content_length") ?? MIN_LENGTH, maxMessageLength: - adnk(content, "info.limitations.max_message_length") ?? + adnk(content, "info.limitations.max_message_length") ?? MIN_LENGTH, requiresPayment: - adnk(content, "info.limitations.payment_required") ?? false, + adnk(content, "info.limitations.payment_required") ?? + false, minPowDifficulty: - adnk(content, "info.limitations.min_pow_difficulty") ?? 0); + adnk(content, "info.limitations.min_pow_difficulty") ?? + 0); bool get isSuitable => canWrite && diff --git a/lib/utils/access-deeply-nested-key.dart b/lib/utils/access-deeply-nested-key.dart index 738b85e4..51c73dd5 100644 --- a/lib/utils/access-deeply-nested-key.dart +++ b/lib/utils/access-deeply-nested-key.dart @@ -2,9 +2,11 @@ T? accessDeeplyNestedKey(final Map obj, final String path) { dynamic result = obj; for (final subPath in path.split(".")) { - if (result is List - ? result[int.parse(subPath)] - : result.containsKey(subPath)) { + if (result is List) { + final index = int.tryParse(subPath)!; + + result = result[index]; + } else if (result.containsKey(subPath)) { result = result[subPath]; } else { return null; From 3279cbd6b3cbc209f86d46c11f0a4b72e19d2eb3 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 28 Sep 2023 00:05:12 +0200 Subject: [PATCH 6/8] feat: Add hint to relay sheet & improve relay sheet --- lib/api/get-relays-meta.dart | 1 + lib/l10n/app_en.arb | 2 + lib/widgets/RelaySelectSheet.dart | 174 ++++++++++++++++++------------ 3 files changed, 111 insertions(+), 66 deletions(-) diff --git a/lib/api/get-relays-meta.dart b/lib/api/get-relays-meta.dart index 180255f0..c1881157 100644 --- a/lib/api/get-relays-meta.dart +++ b/lib/api/get-relays-meta.dart @@ -148,6 +148,7 @@ class RelayMeta { // Lower latency = better - Because of this // a factor closer to 0 resembles a HIGHER weight // We prioritize read latency as we want to be able to provide a fast app + // Lower score = better return (connectionLatencies.average * 0.9 + readLatencies.average * 0.5 + writeLatencies.average) + diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bf96f172..45c99ed1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -110,6 +110,8 @@ } }, "relaySelectSheet_selectRandomRelays": "Select {count} random Nostr Relays", + "relaySelectSheet_loadingRelaysMeta": "Loading Nostr Relays information...", + "relaySelectSheet_hint": "Relays are sorted from best to worst in ascending order. The best relays are at the top.", "taskAction_start": "Start Task", "taskAction_started_title": "Task started", "taskAction_started_description": "Task started at {date}", diff --git a/lib/widgets/RelaySelectSheet.dart b/lib/widgets/RelaySelectSheet.dart index 5f474575..c4d2feac 100644 --- a/lib/widgets/RelaySelectSheet.dart +++ b/lib/widgets/RelaySelectSheet.dart @@ -182,8 +182,9 @@ class _RelaySelectSheetState extends State { setState(() { availableRelays.retainWhere(suitableRelays.contains); availableRelays.sort( - (a, b) => relayMeta[a]!.score > relayMeta[b]!.score ? -1 : 1, + (a, b) => relayMeta[a]!.score > relayMeta[b]!.score ? 1 : -1, ); + loadStatus = LoadStatus.success; }); } @@ -232,7 +233,6 @@ class _RelaySelectSheetState extends State { availableRelays ..clear() ..addAll(relays); - loadStatus = LoadStatus.success; _filterRelaysFromMeta(); setState(() {}); @@ -270,77 +270,119 @@ class _RelaySelectSheetState extends State { final length = allRelays.length + (isValueNew ? 1 : 0); - return ListView.builder( + return SingleChildScrollView( controller: draggableController, - itemCount: length, - itemBuilder: (context, rawIndex) { - if (isValueNew && rawIndex == 0) { - return PlatformWidget( - material: (context, _) => ListTile( - title: Text( - l10n.addNewValueLabel(_newValue), - ), - leading: const Icon( - Icons.add, + child: Column( + children: [ + if (loadStatus == LoadStatus.loading) + Padding( + padding: const EdgeInsets.all(MEDIUM_SPACE), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox.square( + dimension: 20, + child: PlatformCircularProgressIndicator(), + ), + const SizedBox(width: MEDIUM_SPACE), + Text(l10n.relaySelectSheet_loadingRelaysMeta), + ], ), - onTap: () { - widget.controller.add(_searchController.value.text); - _searchController.clear(); - }, - ), - cupertino: (context, _) => CupertinoButton( - child: Text( - l10n.addNewValueLabel(_newValue), + ) + else + Padding( + padding: const EdgeInsets.all(MEDIUM_SPACE), + child: Row( + children: [ + Icon( + context.platformIcons.info, + color: getCaptionTextStyle(context).color, + ), + const SizedBox(width: MEDIUM_SPACE), + Flexible( + child: Text( + l10n.relaySelectSheet_hint, + style: getCaptionTextStyle(context), + ), + ), + ], ), - onPressed: () { - widget.controller.add(_searchController.value.text); - _searchController.clear(); - }, - ), - ); - } - - final index = isValueNew ? rawIndex - 1 : rawIndex; - final relay = allRelays[index]; - final meta = relayMeta[relay]; - - return PlatformWidget( - material: (context, _) => CheckboxListTile( - title: Text( - relay.length >= 6 ? relay.substring(6) : relay, ), - subtitle: meta == null ? null : Text(meta.score.toString()), - value: widget.controller.relays.contains(relay), - onChanged: (newValue) { - if (newValue == null) { - return; + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: length, + itemBuilder: (context, rawIndex) { + if (isValueNew && rawIndex == 0) { + return PlatformWidget( + material: (context, _) => ListTile( + title: Text( + l10n.addNewValueLabel(_newValue), + ), + leading: const Icon( + Icons.add, + ), + onTap: () { + widget.controller.add(_searchController.value.text); + _searchController.clear(); + }, + ), + cupertino: (context, _) => CupertinoButton( + child: Text( + l10n.addNewValueLabel(_newValue), + ), + onPressed: () { + widget.controller.add(_searchController.value.text); + _searchController.clear(); + }, + ), + ); } - if (newValue) { - widget.controller.add(relay); - } else { - widget.controller.remove(relay); - } + final index = isValueNew ? rawIndex - 1 : rawIndex; + final relay = allRelays[index]; + final meta = relayMeta[relay]; + + return PlatformWidget( + material: (context, _) => CheckboxListTile( + title: Text( + relay.length >= 6 ? relay.substring(6) : relay, + ), + subtitle: meta == null ? null : Text(meta.description), + value: widget.controller.relays.contains(relay), + onChanged: (newValue) { + if (newValue == null) { + return; + } + + if (newValue) { + widget.controller.add(relay); + } else { + widget.controller.remove(relay); + } + }, + ), + cupertino: (context, _) => CupertinoListTile( + title: Text( + relay.length >= 6 ? relay.substring(6) : relay, + ), + subtitle: meta == null ? null : Text(meta.description), + trailing: CupertinoSwitch( + value: widget.controller.relays.contains(relay), + onChanged: (newValue) { + if (newValue) { + widget.controller.add(relay); + } else { + widget.controller.remove(relay); + } + }, + ), + ), + ); }, ), - cupertino: (context, _) => CupertinoListTile( - title: Text( - relay.length >= 6 ? relay.substring(6) : relay, - ), - subtitle: meta == null ? null : Text(meta.description), - trailing: CupertinoSwitch( - value: widget.controller.relays.contains(relay), - onChanged: (newValue) { - if (newValue) { - widget.controller.add(relay); - } else { - widget.controller.remove(relay); - } - }, - ), - ), - ); - }, + ], + ), ); }, ); @@ -357,7 +399,7 @@ class _RelaySelectSheetState extends State { miuiIsGapless: true, child: Column( children: [ - if (loadStatus == LoadStatus.loading) + if (loadStatus == LoadStatus.loading && availableRelays.isEmpty) Expanded( child: Center( child: PlatformCircularProgressIndicator(), From b521edbb2c08dcd8b087afd4a8414caeb06f1515 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 28 Sep 2023 00:54:56 +0200 Subject: [PATCH 7/8] fix: Improve RelaySelectSheet.dart --- lib/widgets/RelaySelectSheet.dart | 206 +++++++++++++++--------------- 1 file changed, 101 insertions(+), 105 deletions(-) diff --git a/lib/widgets/RelaySelectSheet.dart b/lib/widgets/RelaySelectSheet.dart index c4d2feac..765db044 100644 --- a/lib/widgets/RelaySelectSheet.dart +++ b/lib/widgets/RelaySelectSheet.dart @@ -268,121 +268,117 @@ class _RelaySelectSheetState extends State { final allRelays = List.from( [...widget.controller.relays, ...uncheckedFoundRelays]); - final length = allRelays.length + (isValueNew ? 1 : 0); - - return SingleChildScrollView( + return ListView.builder( controller: draggableController, - child: Column( - children: [ - if (loadStatus == LoadStatus.loading) - Padding( - padding: const EdgeInsets.all(MEDIUM_SPACE), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox.square( - dimension: 20, - child: PlatformCircularProgressIndicator(), - ), - const SizedBox(width: MEDIUM_SPACE), - Text(l10n.relaySelectSheet_loadingRelaysMeta), - ], + // Add 2 so we can show and widgets + itemCount: allRelays.length + 2, + itemBuilder: (context, rawIndex) { + if (rawIndex == 0) { + if (isValueNew) { + return PlatformWidget( + material: (context, _) => ListTile( + title: Text( + l10n.addNewValueLabel(_newValue), + ), + leading: const Icon( + Icons.add, + ), + onTap: () { + widget.controller.add(_searchController.value.text); + _searchController.clear(); + }, ), - ) - else - Padding( - padding: const EdgeInsets.all(MEDIUM_SPACE), - child: Row( - children: [ - Icon( - context.platformIcons.info, - color: getCaptionTextStyle(context).color, - ), - const SizedBox(width: MEDIUM_SPACE), - Flexible( - child: Text( - l10n.relaySelectSheet_hint, - style: getCaptionTextStyle(context), - ), - ), - ], + cupertino: (context, _) => CupertinoButton( + child: Text( + l10n.addNewValueLabel(_newValue), + ), + onPressed: () { + widget.controller.add(_searchController.value.text); + _searchController.clear(); + }, ), - ), - ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: length, - itemBuilder: (context, rawIndex) { - if (isValueNew && rawIndex == 0) { - return PlatformWidget( - material: (context, _) => ListTile( - title: Text( - l10n.addNewValueLabel(_newValue), - ), - leading: const Icon( - Icons.add, - ), - onTap: () { - widget.controller.add(_searchController.value.text); - _searchController.clear(); - }, + ); + } + return Container(); + } + + if (rawIndex == 1) { + return loadStatus == LoadStatus.loading + ? Padding( + padding: const EdgeInsets.all(MEDIUM_SPACE), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox.square( + dimension: 20, + child: PlatformCircularProgressIndicator(), + ), + const SizedBox(width: MEDIUM_SPACE), + Text(l10n.relaySelectSheet_loadingRelaysMeta), + ], ), - cupertino: (context, _) => CupertinoButton( - child: Text( - l10n.addNewValueLabel(_newValue), - ), - onPressed: () { - widget.controller.add(_searchController.value.text); - _searchController.clear(); - }, + ) + : Padding( + padding: const EdgeInsets.all(MEDIUM_SPACE), + child: Row( + children: [ + Icon( + context.platformIcons.info, + color: getCaptionTextStyle(context).color, + ), + const SizedBox(width: MEDIUM_SPACE), + Flexible( + child: Text( + l10n.relaySelectSheet_hint, + style: getCaptionTextStyle(context), + ), + ), + ], ), ); - } + } - final index = isValueNew ? rawIndex - 1 : rawIndex; - final relay = allRelays[index]; - final meta = relayMeta[relay]; + final index = rawIndex - 1; + final relay = allRelays[index]; + final meta = relayMeta[relay]; - return PlatformWidget( - material: (context, _) => CheckboxListTile( - title: Text( - relay.length >= 6 ? relay.substring(6) : relay, - ), - subtitle: meta == null ? null : Text(meta.description), - value: widget.controller.relays.contains(relay), - onChanged: (newValue) { - if (newValue == null) { - return; - } - - if (newValue) { - widget.controller.add(relay); - } else { - widget.controller.remove(relay); - } - }, - ), - cupertino: (context, _) => CupertinoListTile( - title: Text( - relay.length >= 6 ? relay.substring(6) : relay, - ), - subtitle: meta == null ? null : Text(meta.description), - trailing: CupertinoSwitch( - value: widget.controller.relays.contains(relay), - onChanged: (newValue) { - if (newValue) { - widget.controller.add(relay); - } else { - widget.controller.remove(relay); - } - }, - ), - ), - ); + return PlatformWidget( + material: (context, _) => CheckboxListTile( + title: Text( + relay.length >= 6 ? relay.substring(6) : relay, + ), + subtitle: meta == null ? null : Text(meta.description), + value: widget.controller.relays.contains(relay), + onChanged: (newValue) { + if (newValue == null) { + return; + } + + if (newValue) { + widget.controller.add(relay); + } else { + widget.controller.remove(relay); + } }, ), - ], - ), + cupertino: (context, _) => CupertinoListTile( + title: Text( + relay.length >= 6 ? relay.substring(6) : relay, + ), + subtitle: meta == null ? null : Text(meta.description), + trailing: CupertinoSwitch( + value: widget.controller.relays.contains(relay), + onChanged: (newValue) { + if (newValue) { + widget.controller.add(relay); + } else { + widget.controller.remove(relay); + } + }, + ), + ), + ); + }, ); }, ); From 1c6a2a9d7ea5e44685cfb8689fd6edf05d23c793 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Thu, 28 Sep 2023 09:48:54 +0200 Subject: [PATCH 8/8] fix: Fix out of range --- lib/widgets/RelaySelectSheet.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/widgets/RelaySelectSheet.dart b/lib/widgets/RelaySelectSheet.dart index 765db044..55488fd5 100644 --- a/lib/widgets/RelaySelectSheet.dart +++ b/lib/widgets/RelaySelectSheet.dart @@ -5,17 +5,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_logs/flutter_logs.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:latlong2/latlong.dart'; import 'package:locus/api/get-relays-meta.dart'; import 'package:locus/constants/spacing.dart'; import 'package:locus/constants/values.dart'; -import 'package:locus/services/location_point_service.dart'; import 'package:locus/utils/load_status.dart'; -import 'package:locus/utils/nostr_fetcher/NostrSocket.dart'; import 'package:locus/utils/theme.dart'; import 'package:locus/widgets/BottomSheetFilterBuilder.dart'; import 'package:locus/widgets/ModalSheet.dart'; -import 'package:nostr/nostr.dart'; import '../api/nostr-relays.dart'; import '../utils/cache.dart'; @@ -338,7 +334,7 @@ class _RelaySelectSheetState extends State { ); } - final index = rawIndex - 1; + final index = rawIndex - 2; final relay = allRelays[index]; final meta = relayMeta[relay];