diff --git a/lib/screens/app_stack_screen.dart b/lib/screens/app_stack_screen.dart index f047b8a..7f2102b 100644 --- a/lib/screens/app_stack_screen.dart +++ b/lib/screens/app_stack_screen.dart @@ -63,24 +63,30 @@ class _AppStackContentWithApps extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - // Derive identifiers directly from the raw event tags — stable and complete - // regardless of what's been cached locally. The 'a' tag format is - // '32267:pubkey:d-tag'; we extract just the d-tag (identifier) part. - final appIdentifiers = stack.event + final appAddressableIds = stack.event .getTagSetValues('a') .where((id) => id.startsWith('32267:')) - .map((id) => id.split(':').skip(2).join(':')) .toSet(); - if (appIdentifiers.isEmpty) { + if (appAddressableIds.isEmpty) { return _AppStackContent(stack: stack, apps: const []); } - // Query apps with release and metadata relationships (same pattern as search/user screens) + final authors = {}; + final identifiers = {}; + for (final id in appAddressableIds) { + final parts = id.split(':'); + if (parts.length >= 3) { + authors.add(parts[1]); + identifiers.add(parts.skip(2).join(':')); + } + } + final appsState = ref.watch( query( + authors: authors, tags: { - '#d': appIdentifiers, + '#d': identifiers, '#f': {'android-arm64-v8a'}, }, and: (app) => { @@ -96,9 +102,9 @@ class _AppStackContentWithApps extends HookConsumerWidget { ), ); - // Preserve the original stack order where possible - final appsMap = {for (final app in appsState.models) app.identifier: app}; - final orderedApps = appIdentifiers + // Key by addressable ID and preserve the original stack order + final appsMap = {for (final app in appsState.models) app.id: app}; + final orderedApps = appAddressableIds .map((id) => appsMap[id]) .whereType() .toList(); diff --git a/lib/screens/app_stacks_screen.dart b/lib/screens/app_stacks_screen.dart index 933b171..e403c6c 100644 --- a/lib/screens/app_stacks_screen.dart +++ b/lib/screens/app_stacks_screen.dart @@ -156,18 +156,21 @@ class AppStacksScreen extends HookConsumerWidget { }; final isAuthorsLoading = authorsState is StorageLoading; - final allPreviewIdentifiers = {}; + final allPreviewIds = {}; final stackPreviewIds = >{}; for (final stack in items) { - final ids = getPreviewIdentifiers(stack); + final ids = getPreviewAddressableIds(stack); stackPreviewIds[stack.id] = ids; - allPreviewIdentifiers.addAll(ids); + allPreviewIds.addAll(ids); } - final previewAppsState = allPreviewIdentifiers.isNotEmpty + final (:authors, :identifiers) = decomposeAddressableIds(allPreviewIds); + + final previewAppsState = allPreviewIds.isNotEmpty ? ref.watch( query( - tags: {'#d': allPreviewIdentifiers}, + authors: authors, + tags: {'#d': identifiers}, source: const LocalAndRemoteSource( relays: 'AppCatalog', stream: false, @@ -179,7 +182,7 @@ class AppStacksScreen extends HookConsumerWidget { final appsMap = { for (final app in previewAppsState?.models ?? []) - app.identifier: app, + app.id: app, }; final isInitialLoading = state.firstPage is StorageLoading && items.isEmpty; @@ -345,19 +348,22 @@ class _MigrationBanner extends HookConsumerWidget { profile.pubkey: profile, }; - final allPreviewIdentifiers = {}; + final allPreviewIds = {}; final stackPreviewIds = >{}; for (final stack in stacks) { - final ids = getPreviewIdentifiers(stack); + final ids = getPreviewAddressableIds(stack); stackPreviewIds[stack.id] = ids; - allPreviewIdentifiers.addAll(ids); + allPreviewIds.addAll(ids); } + final (:authors, :identifiers) = decomposeAddressableIds(allPreviewIds); + // Query preview apps - final previewAppsState = allPreviewIdentifiers.isNotEmpty + final previewAppsState = allPreviewIds.isNotEmpty ? ref.watch( query( - tags: {'#d': allPreviewIdentifiers}, + authors: authors, + tags: {'#d': identifiers}, source: const LocalAndRemoteSource( relays: 'AppCatalog', stream: false, @@ -368,7 +374,7 @@ class _MigrationBanner extends HookConsumerWidget { : null; final appsMap = { for (final app in previewAppsState?.models ?? []) - app.identifier: app, + app.id: app, }; return Container( diff --git a/lib/widgets/app_stack_container.dart b/lib/widgets/app_stack_container.dart index 01a3e6f..3015316 100644 --- a/lib/widgets/app_stack_container.dart +++ b/lib/widgets/app_stack_container.dart @@ -22,17 +22,30 @@ Set getRawAppTagValues(AppStack stack) { return stack.event.getTagSetValues('a'); } -/// Extract just the d-tag (identifier) from a full addressable id -String? extractStackIdentifier(String addressableId) { - final parts = addressableId.split(':'); - return parts.length >= 3 ? parts.sublist(2).join(':') : null; +/// Helper to compute preview app addressable IDs for a stack (3 apps max). +/// Returns full addressable IDs (e.g. '32267:pubkey:identifier'). +List getPreviewAddressableIds(AppStack stack) { + final rawTags = getRawAppTagValues(stack) + .where((id) => id.startsWith('32267:')) + .toList() + ..shuffle(Random(stack.id.hashCode)); + return rawTags.take(3).toList(); } -/// Helper to compute preview app identifiers for a stack (3 apps max) -List getPreviewIdentifiers(AppStack stack) { - final rawTags = getRawAppTagValues(stack).toList() - ..shuffle(Random(stack.id.hashCode)); - return rawTags.take(3).map(extractStackIdentifier).whereType().toList(); +/// Decompose addressable IDs (e.g. '32267:pubkey:identifier') into +/// the sets of authors and identifiers needed for a query filter. +({Set authors, Set identifiers}) decomposeAddressableIds( + Iterable addressableIds) { + final authors = {}; + final identifiers = {}; + for (final id in addressableIds) { + final parts = id.split(':'); + if (parts.length >= 3) { + authors.add(parts[1]); + identifiers.add(parts.skip(2).join(':')); + } + } + return (authors: authors, identifiers: identifiers); } /// Seed generated once per app session for stable shuffle order @@ -95,18 +108,21 @@ class AppStackContainer extends HookConsumerWidget { final displayedStacks = sortedStacks.take(visibleCount.value).toList(); // Batch load preview apps for displayed stacks (3 per stack) - final allPreviewIdentifiers = {}; + final allPreviewIds = {}; final stackPreviewIds = >{}; for (final stack in displayedStacks) { - final ids = getPreviewIdentifiers(stack); + final ids = getPreviewAddressableIds(stack); stackPreviewIds[stack.id] = ids; - allPreviewIdentifiers.addAll(ids); + allPreviewIds.addAll(ids); } - final previewAppsState = allPreviewIdentifiers.isNotEmpty + final (:authors, :identifiers) = decomposeAddressableIds(allPreviewIds); + + final previewAppsState = allPreviewIds.isNotEmpty ? ref.watch( query( - tags: {'#d': allPreviewIdentifiers}, + authors: authors, + tags: {'#d': identifiers}, source: const LocalAndRemoteSource( relays: 'AppCatalog', stream: false, @@ -118,7 +134,7 @@ class AppStackContainer extends HookConsumerWidget { final appsMap = { for (final app in previewAppsState?.models ?? []) - app.identifier: app, + app.id: app, }; // Infinite horizontal scroll: load more when near end