Stacks should query apps with full pubkey + app ID

This commit is contained in:
franzap
2026-04-07 15:45:44 -03:00
parent 6fe73ed47d
commit 5eda05bb21
3 changed files with 66 additions and 38 deletions
+17 -11
View File
@@ -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 = <String>{};
final identifiers = <String>{};
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<App>(
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<App>()
.toList();
+18 -12
View File
@@ -156,18 +156,21 @@ class AppStacksScreen extends HookConsumerWidget {
};
final isAuthorsLoading = authorsState is StorageLoading;
final allPreviewIdentifiers = <String>{};
final allPreviewIds = <String>{};
final stackPreviewIds = <String, List<String>>{};
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<App>(
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>[])
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 = <String>{};
final allPreviewIds = <String>{};
final stackPreviewIds = <String, List<String>>{};
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<App>(
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>[])
app.identifier: app,
app.id: app,
};
return Container(
+31 -15
View File
@@ -22,17 +22,30 @@ Set<String> 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<String> 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<String> getPreviewIdentifiers(AppStack stack) {
final rawTags = getRawAppTagValues(stack).toList()
..shuffle(Random(stack.id.hashCode));
return rawTags.take(3).map(extractStackIdentifier).whereType<String>().toList();
/// Decompose addressable IDs (e.g. '32267:pubkey:identifier') into
/// the sets of authors and identifiers needed for a query filter.
({Set<String> authors, Set<String> identifiers}) decomposeAddressableIds(
Iterable<String> addressableIds) {
final authors = <String>{};
final identifiers = <String>{};
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 = <String>{};
final allPreviewIds = <String>{};
final stackPreviewIds = <String, List<String>>{};
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<App>(
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>[])
app.identifier: app,
app.id: app,
};
// Infinite horizontal scroll: load more when near end