diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 2457a29f..22e18d8d 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -12,6 +12,9 @@
+
+
+
@@ -79,6 +82,22 @@
android:scheme="https" />
+
+
+
+
+
+
+
+
+
() {
+ if (!registry.hasPlugin("BackgroundLocatorPlugin")) {
+ GeneratedPluginRegistrant.register(with: registry)
+ }
+}
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
@@ -8,6 +15,7 @@ import Flutter
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
+ BackgroundLocatorPlugin.setPluginRegistrantCallback(registerPlugins)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
diff --git a/lib/App.dart b/lib/App.dart
index fdc0ffab..e540f0df 100644
--- a/lib/App.dart
+++ b/lib/App.dart
@@ -6,7 +6,7 @@ import 'package:locus/constants/colors.dart';
import 'package:locus/screens/BiometricsRequiredStartupScreen.dart';
import 'package:locus/screens/LocationsOverviewScreen.dart';
import 'package:locus/screens/WelcomeScreen.dart';
-import 'package:locus/services/settings_service.dart';
+import 'package:locus/services/settings_service/index.dart';
import 'package:locus/utils/PageRoute.dart';
import 'package:locus/utils/color.dart';
import 'package:locus/widgets/DismissKeyboard.dart';
diff --git a/lib/api/get-locations.dart b/lib/api/get-locations.dart
deleted file mode 100644
index 1ec9b8a9..00000000
--- a/lib/api/get-locations.dart
+++ /dev/null
@@ -1,103 +0,0 @@
-import 'dart:async';
-
-import 'package:cryptography/cryptography.dart';
-import 'package:flutter/animation.dart';
-import 'package:flutter_logs/flutter_logs.dart';
-import 'package:locus/constants/values.dart';
-import 'package:locus/services/location_point_service.dart';
-import 'package:nostr/nostr.dart';
-
-import 'nostr-fetch.dart';
-
-VoidCallback getLocations({
- required final String nostrPublicKey,
- required final SecretKey encryptionPassword,
- required final List relays,
- required void Function(LocationPointService) onLocationFetched,
- required void Function() onEnd,
- final VoidCallback? onEmptyEnd,
- int? limit,
- DateTime? from,
- DateTime? until,
-}) {
- final request = Request(generate64RandomHexChars(), [
- Filter(
- kinds: [1000],
- authors: [nostrPublicKey],
- limit: limit,
- until:
- until == null ? null : (until.millisecondsSinceEpoch / 1000).floor(),
- since: from == null ? null : (from.millisecondsSinceEpoch / 1000).floor(),
- ),
- ]);
-
- final nostrFetch = NostrFetch(
- relays: relays,
- request: request,
- );
-
- return nostrFetch.fetchEvents(
- onEvent: (message, _) async {
- FlutterLogs.logInfo(
- LOG_TAG,
- "GetLocations",
- "New message. Decrypting...",
- );
-
- try {
- final location = await LocationPointService.fromEncrypted(
- message.message.content,
- encryptionPassword,
- );
-
- FlutterLogs.logInfo(
- LOG_TAG,
- "GetLocations",
- "New message. Decrypting... Done!",
- );
-
- onLocationFetched(location);
- } catch (error) {
- FlutterLogs.logError(
- LOG_TAG,
- "GetLocations",
- "Error decrypting message: $error",
- );
-
- return;
- }
- },
- onEnd: onEnd,
- onEmptyEnd: onEmptyEnd,
- );
-}
-
-Future> getLocationsAsFuture({
- required final String nostrPublicKey,
- required final SecretKey encryptionPassword,
- required final List relays,
- int? limit,
- DateTime? from,
- DateTime? until,
-}) async {
- final completer = Completer>();
-
- final List locations = [];
-
- getLocations(
- nostrPublicKey: nostrPublicKey,
- encryptionPassword: encryptionPassword,
- relays: relays,
- limit: limit,
- from: from,
- until: until,
- onLocationFetched: (final LocationPointService location) {
- locations.add(location);
- },
- onEnd: () {
- completer.complete(locations);
- },
- );
-
- return completer.future;
-}
diff --git a/lib/api/get-relays-meta.dart b/lib/api/get-relays-meta.dart
new file mode 100644
index 00000000..c1881157
--- /dev/null
+++ b/lib/api/get-relays-meta.dart
@@ -0,0 +1,185 @@
+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';
+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") ??
+ MIN_LENGTH,
+ maxMessageLength:
+ adnk(content, "info.limitations.max_message_length") ??
+ MIN_LENGTH,
+ 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
+ // Lower score = better
+ return (connectionLatencies.average * 0.9 +
+ readLatencies.average * 0.5 +
+ writeLatencies.average) +
+ (maxContentLength - MIN_LENGTH) * 0.0001;
+ }
+}
+
+// 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: 1000,
+ from: DateTime.now().subtract(2.hours),
+ authors: ["b3b0d247f66bf40c4c9f4ce721abfe1fd3b7529fbc1ea5e64d5f0f8df3a4b6e6"],
+);
+
+Future