From 6d8efa4420b64d1dca063df16a8a537b750bfd90 Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Tue, 15 Oct 2024 10:39:38 -0300 Subject: [PATCH] Fix latest release queries (remote and local), load users once --- lib/models/app.dart | 95 ++++++++++++++-------- lib/models/nostr_adapter.dart | 5 +- lib/models/release.dart | 6 +- lib/navigation/router.dart | 7 ++ lib/widgets/latest_releases_container.dart | 41 +++++++--- 5 files changed, 102 insertions(+), 52 deletions(-) diff --git a/lib/models/app.dart b/lib/models/app.dart index 352beec..f9e5a1b 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -43,7 +43,7 @@ class App extends BaseApp with DataModelMixin { Map toJson() => super.toMap(); Release? get latestRelease { - return releases.ordered.firstOrNull; + return releases.toList().sortedByLatest.firstOrNull; } FileMetadata? get latestMetadata { @@ -168,48 +168,68 @@ class App extends BaseApp with DataModelMixin { mixin AppAdapter on Adapter { Future> fetchAppModels(Map params) async { - final apps = await super.findAll( - params: { - ...params, - '#f': ['android-arm64-v8a'], - }, - ); - // Find all appid@version ($3) as we need to pick one tag to query on - // (filters by kind ($1) and pubkey ($2) done locally) - final latestReleaseIdentifiers = - apps.map((app) => app.linkedReplaceableEvents.firstOrNull?.$3).nonNulls; - final releases = await ref.releases.findAll( - params: {'#d': latestReleaseIdentifiers}, - ); + final byRelease = params.remove('by-release'); - // TODO: Deprecated, will be removed - // Some developers without access to the latest zapstore-cli - // have not published their apps with latest release identifiers - // so load as usual - final deprecatedApps = - apps.where((app) => app.linkedReplaceableEvents.isEmpty); - final deprecatedReleases = await ref.releases.findAll( - params: { - '#a': - deprecatedApps.map((app) => app.getReplaceableEventLink().formatted) - }, - ); + late final List apps; + late final List releases; + if (byRelease != null) { + // This param fetches releases with params and then apps + releases = await ref.releases.findAll(params: params); + final appIdentifiers = + releases.map((r) => r.linkedReplaceableEvents.first.$3).nonNulls; + apps = await super.findAll( + params: { + '#d': appIdentifiers, + '#f': ['android-arm64-v8a'], + }, + ); + } else { + apps = await super.findAll( + params: { + ...params, + '#f': ['android-arm64-v8a'], + }, + ); + // Find all appid@version ($3) as we need to pick one tag to query on + // (filters by kind ($1) and pubkey ($2) done locally) + final latestReleaseIdentifiers = apps + .map((app) => app.linkedReplaceableEvents.firstOrNull?.$3) + .nonNulls; + releases = await ref.releases.findAll( + params: {'#d': latestReleaseIdentifiers}, + ); - final groupedDeprecatedReleases = - deprecatedReleases.groupListsBy((r) => r.app.value!); - for (final e in groupedDeprecatedReleases.entries) { - final mostRecentRelease = - e.value.sorted((a, b) => b.createdAt!.compareTo(a.createdAt!)).first; - releases.add(mostRecentRelease); + // TODO: Deprecated, will be removed + // Some developers without access to the latest zapstore-cli + // have not published their apps with latest release identifiers + // so load as usual + final deprecatedApps = + apps.where((app) => app.linkedReplaceableEvents.isEmpty); + final deprecatedReleases = await ref.releases.findAll( + params: { + '#a': deprecatedApps + .map((app) => app.getReplaceableEventLink().formatted) + }, + ); + + final groupedDeprecatedReleases = + deprecatedReleases.groupListsBy((r) => r.app.value!); + for (final e in groupedDeprecatedReleases.entries) { + final mostRecentRelease = e.value.sortedByLatest.first; + releases.add(mostRecentRelease); + } + // End deprecated } - // End deprecated final metadataIds = releases.map((r) => r.linkedEvents).nonNulls.expand((_) => _); + // For now load users only once final userIds = { - for (final app in apps) app.signer.id, - for (final app in apps) app.developer.id + for (final app in apps) + if (!app.signer.isPresent) app.signer.id, + for (final app in apps) + if (!app.developer.isPresent) app.developer.id, }.nonNulls; // Metadata and users probably go to separate relays @@ -335,6 +355,11 @@ Future _isHashMismatch(String path, String hash) async { }); } +extension AppExt on Iterable { + List get sortedByLatest => + sorted((a, b) => b.createdAt!.compareTo(a.createdAt!)); +} + // class sealed class AppInstallProgress {} diff --git a/lib/models/nostr_adapter.dart b/lib/models/nostr_adapter.dart index 341f8ad..1448e79 100644 --- a/lib/models/nostr_adapter.dart +++ b/lib/models/nostr_adapter.dart @@ -1,9 +1,10 @@ +import 'package:collection/collection.dart'; import 'package:flutter_data/flutter_data.dart'; import 'package:purplebase/purplebase.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.zap.store']; +// const kAppRelays = ['ws://10.0.2.2:3000']; +const kAppRelays = ['wss://relay.zap.store']; const kSocialRelays = ['wss://relay.primal.net', 'wss://relay.nostr.band']; mixin NostrAdapter> on Adapter { diff --git a/lib/models/release.dart b/lib/models/release.dart index 6611e8c..80d0ced 100644 --- a/lib/models/release.dart +++ b/lib/models/release.dart @@ -46,7 +46,7 @@ mixin ReleaseAdapter on Adapter { } } -extension HasManyReleaseX on HasMany { - List get ordered => - toList().sorted((a, b) => b.createdAt!.compareTo(a.createdAt!)); +extension ReleaseExt on Iterable { + List get sortedByLatest => + sorted((a, b) => b.createdAt!.compareTo(a.createdAt!)); } diff --git a/lib/navigation/router.dart b/lib/navigation/router.dart index 8a26c92..d0fb989 100644 --- a/lib/navigation/router.dart +++ b/lib/navigation/router.dart @@ -175,6 +175,13 @@ final dataLibrariesInitializer = FutureProvider((ref) async { // In this initial phase, load there more or less fixed curation sets here await ref.appCurationSets.findAll(); + await ref.apps.findAll( + params: { + 'by-release': true, + 'limit': 10, + }, + ); + // Handle deep links final appLinksSub = appLinks.uriLinkStream.listen((uri) async { if (uri.scheme == "zapstore") { diff --git a/lib/widgets/latest_releases_container.dart b/lib/widgets/latest_releases_container.dart index a224c64..e5d3973 100644 --- a/lib/widgets/latest_releases_container.dart +++ b/lib/widgets/latest_releases_container.dart @@ -1,12 +1,12 @@ import 'dart:async'; import 'package:async_button_builder/async_button_builder.dart'; -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; 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/models/release.dart'; import 'package:zapstore/widgets/app_card.dart'; class LatestReleasesContainer extends HookConsumerWidget { @@ -53,6 +53,8 @@ class LatestReleasesContainer extends HookConsumerWidget { if (state.isLoading) for (final _ in List.generate(3, (_) => _)) AppCard(app: null), if (state.hasValue) + // NOTE: Since we're showing apps but it's really a list of releases + // apps will appear repeated, to convert to set for (final app in state.value!) AppCard(app: app), if (state.hasValue) AsyncButtonBuilder( @@ -96,31 +98,46 @@ class LatestReleasesAppNotifier extends AutoDisposeAsyncNotifier> { @override Future> build() async { // TODO: Should be ref.watching a pool state change (from purplebase) - final timer = Timer.periodic(Duration(minutes: 1), (_) => fetch()); + final timer = Timer.periodic(Duration(minutes: 10), (_) => fetch()); ref.onDispose(timer.cancel); - // Trigger fetch ONLY on the most recent 10 (next=false) - await fetch(); + + // Do not fetch here as first 10 are preloaded + // await fetch(); return localFetch(); } List localFetch() { - final apps = ref.apps + // Find all releases that are the latest of each, and sort chronologically + final releases = ref.releases .findAllLocal() - .sorted((a, b) => b.createdAt!.compareTo(a.createdAt!)) + .where((r) => r.app.value?.latestMetadata != null) + .sortedByLatest; + // Set timestamp of oldest, to prepare for next query + if (releases.isNotEmpty) { + oldestTimestamp = releases.last.createdAtMs; + } + // Return only (first 10 * page) releases that have an associated app + // (it should be the case, but keep as we migrate + // from older event format) + return releases + .map((r) => r.app.value) + .nonNulls + .toSet() .take(page * 10) .toList(); - if (apps.isNotEmpty) { - oldestTimestamp = apps.last.createdAtMs; - } - return apps; } Future fetch({bool next = false}) async { if (next) { page++; } - await ref.apps - .findAll(params: {'limit': 10, 'until': next ? oldestTimestamp : null}); + await ref.apps.findAll( + params: { + 'by-release': true, + 'limit': 10, + 'until': next ? oldestTimestamp : null + }, + ); update((_) => localFetch()); } }