diff --git a/CHANGELOG.md b/CHANGELOG.md index 44399a5..f358dff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 0.1.8 + + - Bugfix: Prevent unnecessary relay initialization + - Bugfix: [purplebase] Block until enough relay EOSEs + - Bugfix: Fix regression, offline-first models + # 0.1.7 - Performance: Faster, background downloads (#172) diff --git a/lib/models/nostr_adapter.dart b/lib/models/nostr_adapter.dart index 0843631..fdb61d4 100644 --- a/lib/models/nostr_adapter.dart +++ b/lib/models/nostr_adapter.dart @@ -13,11 +13,12 @@ import 'package:zapstore/widgets/latest_releases_container.dart'; // NOTE: Very important to use const in relay args to preserve equality in Riverpod families // const kAppRelays = {'ws://10.0.2.2:3000'}; const kAppRelays = {'wss://relay.zapstore.dev'}; -const kSocialRelays = {'wss://relay.damus.io', 'wss://relay.nostr.band'}; +const kSocialRelays = {'wss://relay.damus.io', 'wss://relay.primal.net'}; mixin NostrAdapter> on Adapter { RelayMessageNotifier get relay => ref.read(relayProviderFamily(kAppRelays).notifier); + RelayMessageNotifier get socialRelays => ref.read(relayProviderFamily(kSocialRelays).notifier); @@ -25,24 +26,29 @@ mixin NostrAdapter> on Adapter { Future onInitialized() async { await super.onInitialized(); - // Upon adapter initialization, configure relays - // with the event verification caching function - relay.configure( - isEventVerified: (Map map) { - // If replaceable, we check for that ID - final identifier = (map['tags'] as Iterable) - .firstWhereOrNull((t) => t[0] == 'd')?[1] - ?.toString(); - final id = identifier != null - ? (map['kind'] as int, map['pubkey'].toString(), identifier) - .formatted - : map['id']; - return ref.apps.nostrAdapter.existsId(id); - }, - ); - socialRelays.configure( - isEventVerified: (map) => ref.apps.nostrAdapter.existsId(map['pubkey']), - ); + // DO NOT run on isolates, the mere fact of calling the + // relay getter will trigger its initialization + // as providers are obviously not cached across isolates + if (!inIsolate) { + // Upon adapter initialization, configure relays + // with the event verification caching function + relay.configure( + isEventVerified: (Map map) { + // If replaceable, we check for that ID + final identifier = (map['tags'] as Iterable) + .firstWhereOrNull((t) => t[0] == 'd')?[1] + ?.toString(); + final id = identifier != null + ? (map['kind'] as int, map['pubkey'].toString(), identifier) + .formatted + : map['id']; + return ref.apps.nostrAdapter.existsId(id); + }, + ); + socialRelays.configure( + isEventVerified: (map) => ref.apps.nostrAdapter.existsId(map['pubkey']), + ); + } } int get kind { @@ -182,7 +188,7 @@ class RelayListenerNotifier extends Notifier { Future fetch() async { await Future.microtask(() async { - await ref.read(latestReleasesAppProvider.notifier).fetch(); + await ref.read(latestReleasesAppProvider.notifier).fetchRemote(); await ref.apps.appAdapter.checkForUpdates(); }); } diff --git a/lib/navigation/app_initializer.dart b/lib/navigation/app_initializer.dart index bec9bee..98fa70f 100644 --- a/lib/navigation/app_initializer.dart +++ b/lib/navigation/app_initializer.dart @@ -31,12 +31,12 @@ final appInitializer = FutureProvider((ref) async { if (ref.appCurationSets.countLocal == 0) { // If we are here, local storage is empty // Preload curation sets - await ref.appCurationSets.findAll(); + await ref.appCurationSets.findAll(syncLocal: true); // Preload updates in the background (no await) ref.apps.appAdapter.checkForUpdates(); } else { // Preload curation sets in the background (no await) - ref.appCurationSets.findAll(); + ref.appCurationSets.findAll(syncLocal: true, background: true); } // Preload zapstore's nostr curation set diff --git a/lib/utils/notifier.dart b/lib/utils/notifier.dart new file mode 100644 index 0000000..a6a850e --- /dev/null +++ b/lib/utils/notifier.dart @@ -0,0 +1,12 @@ +import 'package:flutter_data/flutter_data.dart'; + +abstract class PreloadingStateNotifier extends StateNotifier> { + final Ref ref; + PreloadingStateNotifier(this.ref) : super(AsyncLoading()) { + state = fetchLocal(); + fetchRemote(); + } + + AsyncValue fetchLocal(); + Future fetchRemote(); +} diff --git a/lib/widgets/app_curation_container.dart b/lib/widgets/app_curation_container.dart index b09d24d..c863c9f 100644 --- a/lib/widgets/app_curation_container.dart +++ b/lib/widgets/app_curation_container.dart @@ -18,8 +18,11 @@ class AppCurationContainer extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final scrollController = useScrollController(); final selectedAppCurationSet = ref.watch(_selectedIdProvider); - final appCurationSets = - ref.appCurationSets.findAllLocal().sortedBy((s) => s.createdAt!); + final appCurationSets = ref.appCurationSets + .findAllLocal() + .sortedBy((s) => s.createdAt!) + .reversed + .toList(); // Custom curation set to place nostr set first (as its preloaded) final nostrCurationSet = appCurationSets.firstWhereOrNull( @@ -108,9 +111,12 @@ class AppCurationSetNotifier Future fetch() async { final appCurationSet = await future; - state = AsyncLoading(); + + if (appCurationSet.apps.isEmpty) { + state = AsyncLoading(); + } state = await AsyncValue.guard(() async { - // Only load apps (no releases for now) + // Only load apps (no releases for now, includes=false) // We can use ignoreReturn here as we do not care about releases await ref.apps.findAll( params: { diff --git a/lib/widgets/author_container.dart b/lib/widgets/author_container.dart index 0775e92..6333d86 100644 --- a/lib/widgets/author_container.dart +++ b/lib/widgets/author_container.dart @@ -7,7 +7,7 @@ import 'package:zapstore/widgets/rounded_image.dart'; class AuthorContainer extends StatelessWidget { final User user; final String beforeText; - final String afterText; + final String? afterText; final bool oneLine; final double size; @@ -15,7 +15,7 @@ class AuthorContainer extends StatelessWidget { super.key, required this.user, this.beforeText = 'Signed by', - this.afterText = '', + this.afterText, this.oneLine = true, this.size = 14, }); @@ -49,7 +49,11 @@ class AuthorContainer extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, ), - Text(afterText), + if (afterText != null) + Text( + afterText!, + style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold), + ), ], ) ], diff --git a/lib/widgets/latest_releases_container.dart b/lib/widgets/latest_releases_container.dart index 1801527..6cffdeb 100644 --- a/lib/widgets/latest_releases_container.dart +++ b/lib/widgets/latest_releases_container.dart @@ -6,6 +6,7 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/app.dart'; +import 'package:zapstore/utils/notifier.dart'; import 'package:zapstore/widgets/app_card.dart'; class LatestReleasesContainer extends HookConsumerWidget { @@ -58,7 +59,7 @@ class LatestReleasesContainer extends HookConsumerWidget { onPressed: () async { return ref .read(latestReleasesAppProvider.notifier) - .fetch(next: true); + .fetchRemote(next: true); }, builder: (context, child, callback, state) { return Padding( @@ -83,16 +84,14 @@ class LatestReleasesContainer extends HookConsumerWidget { } } -class LatestReleasesAppNotifier extends StateNotifier>> { +class LatestReleasesAppNotifier extends PreloadingStateNotifier> { DateTime? _oldestCreatedAt; int _page = 1; - final Ref ref; - LatestReleasesAppNotifier(this.ref) : super(AsyncLoading()) { - fetch(); - } + LatestReleasesAppNotifier(super.ref); - Future fetch({bool next = false}) async { + @override + Future fetchRemote({bool next = false}) async { if (next) { _page++; } @@ -107,12 +106,17 @@ class LatestReleasesAppNotifier extends StateNotifier>> { }, ); - state = AsyncData(_fetchLocal()); + state = fetchLocal(); } - List _fetchLocal() { + @override + AsyncValue> fetchLocal() { final model = ref.apps.findAllLocal(); + if (model.isEmpty) { + return AsyncLoading(); + } + final apps = model .where((a) => a.latestMetadata != null) .sortedByLatest @@ -123,7 +127,7 @@ class LatestReleasesAppNotifier extends StateNotifier>> { if (apps.isNotEmpty) { _oldestCreatedAt = apps.last.createdAt; } - return apps; + return AsyncData(apps); } } diff --git a/lib/widgets/signer_and_developer_row.dart b/lib/widgets/signer_and_developer_row.dart index e1c9463..332c403 100644 --- a/lib/widgets/signer_and_developer_row.dart +++ b/lib/widgets/signer_and_developer_row.dart @@ -49,7 +49,7 @@ class SignerAndDeveloperRow extends ConsumerWidget { settingsState.model!.trustedUsers .contains(app.signer.value!) ? '(trusted)' - : '', + : null, ), ], ), diff --git a/pubspec.lock b/pubspec.lock index cd434c1..94c4e75 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -938,8 +938,8 @@ packages: dependency: "direct main" description: path: "." - ref: cbe8342031adf908c8243125e176f52739d400d8 - resolved-ref: cbe8342031adf908c8243125e176f52739d400d8 + ref: "7f0eefd1fd1bcbc947123dc377183dbae58ea97a" + resolved-ref: "7f0eefd1fd1bcbc947123dc377183dbae58ea97a" url: "https://github.com/purplebase/purplebase" source: git version: "0.2.0" diff --git a/pubspec.yaml b/pubspec.yaml index b5681cd..50deee6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: zapstore description: The permissionless app store publish_to: 'none' -version: 0.1.7+17 +version: 0.1.8+18 environment: sdk: '>=3.3.0 <4.0.0' @@ -57,7 +57,7 @@ dependency_overrides: # path: ../purplebase git: url: https://github.com/purplebase/purplebase - ref: cbe8342031adf908c8243125e176f52739d400d8 + ref: 7f0eefd1fd1bcbc947123dc377183dbae58ea97a flutter_data: git: url: https://github.com/zapstore/flutter_data