mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
Stacks fixes
This commit is contained in:
@@ -15,6 +15,10 @@ const kTrustedRelayPubkeys = {
|
||||
const kFranzapPubkey =
|
||||
'726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11';
|
||||
|
||||
/// Zapstore community public key (npub14nl2afh9zsswsp5043zxe2w304afaa496gxe8z2w2rlw84ys92zqlnjx5u)
|
||||
const kZapstoreCommunityPubkey =
|
||||
'acfeaea6e51420e8068fac446ca9d17d7a9ef6a5d20d93894e50fee3d4902a84';
|
||||
|
||||
/// Identifier for storing user saved apps
|
||||
const kAppBookmarksIdentifier = 'zapstore-bookmarks';
|
||||
|
||||
|
||||
+22
-9
@@ -6,7 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:models/models.dart';
|
||||
import 'package:zapstore/screens/main_scaffold.dart';
|
||||
import 'package:zapstore/screens/app_detail_screen.dart';
|
||||
import 'package:zapstore/screens/all_stacks_screen.dart';
|
||||
import 'package:zapstore/screens/stacks_screen.dart';
|
||||
import 'package:zapstore/screens/app_stack_screen.dart';
|
||||
import 'package:zapstore/screens/user_screen.dart';
|
||||
import 'package:zapstore/screens/search_screen.dart';
|
||||
@@ -29,7 +29,8 @@ CustomTransitionPage<void> _noTransitionPage({
|
||||
return CustomTransitionPage<void>(
|
||||
key: state.pageKey,
|
||||
child: child,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) => child,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||
child,
|
||||
transitionDuration: Duration.zero,
|
||||
reverseTransitionDuration: Duration.zero,
|
||||
);
|
||||
@@ -90,10 +91,7 @@ GoRoute _allStacksRoute() {
|
||||
return GoRoute(
|
||||
path: 'stacks',
|
||||
pageBuilder: (context, state) {
|
||||
return _noTransitionPage(
|
||||
state: state,
|
||||
child: const AllStacksScreen(),
|
||||
);
|
||||
return _noTransitionPage(state: state, child: const StacksScreen());
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -137,7 +135,12 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
state: state,
|
||||
child: const SearchScreen(),
|
||||
),
|
||||
routes: [_appDetailRoute(), _stackDetailRoute(), _allStacksRoute(), _userRoute()],
|
||||
routes: [
|
||||
_appDetailRoute(),
|
||||
_stackDetailRoute(),
|
||||
_allStacksRoute(),
|
||||
_userRoute(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -150,7 +153,12 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
state: state,
|
||||
child: const UpdatesScreen(),
|
||||
),
|
||||
routes: [_appDetailRoute(), _stackDetailRoute(), _allStacksRoute(), _userRoute()],
|
||||
routes: [
|
||||
_appDetailRoute(),
|
||||
_stackDetailRoute(),
|
||||
_allStacksRoute(),
|
||||
_userRoute(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -163,7 +171,12 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
state: state,
|
||||
child: const ProfileScreen(),
|
||||
),
|
||||
routes: [_appDetailRoute(), _stackDetailRoute(), _allStacksRoute(), _userRoute()],
|
||||
routes: [
|
||||
_appDetailRoute(),
|
||||
_stackDetailRoute(),
|
||||
_allStacksRoute(),
|
||||
_userRoute(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -2,14 +2,16 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:models/models.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:zapstore/services/package_manager/package_manager.dart';
|
||||
import 'package:zapstore/theme.dart';
|
||||
import 'package:zapstore/utils/extensions.dart';
|
||||
import 'package:zapstore/widgets/app_stack_container.dart';
|
||||
|
||||
const int _kPageSize = 10;
|
||||
|
||||
class AllStacksScreen extends HookConsumerWidget {
|
||||
const AllStacksScreen({super.key});
|
||||
class StacksScreen extends HookConsumerWidget {
|
||||
const StacksScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -25,7 +27,7 @@ class AllStacksScreen extends HookConsumerWidget {
|
||||
tags: {
|
||||
'#f': {platform},
|
||||
},
|
||||
source: const LocalAndRemoteSource(relays: 'social'),
|
||||
source: const LocalAndRemoteSource(relays: 'AppCatalog'),
|
||||
subscriptionPrefix: 'app-all-stacks',
|
||||
schemaFilter: appStackEventFilter,
|
||||
),
|
||||
@@ -49,10 +51,7 @@ class AllStacksScreen extends HookConsumerWidget {
|
||||
final followingPubkeys =
|
||||
contactListState?.models.firstOrNull?.followingPubkeys;
|
||||
|
||||
final allStacks = switch (appStacksState) {
|
||||
StorageData(:final models) => models.toList(),
|
||||
_ => <AppStack>[],
|
||||
};
|
||||
final allStacks = appStacksState.models.toList();
|
||||
|
||||
final sortedStacks = _sortStacks(
|
||||
allStacks,
|
||||
@@ -134,12 +133,32 @@ class AllStacksScreen extends HookConsumerWidget {
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Text('App Stacks', style: context.textTheme.headlineMedium),
|
||||
child: Text(
|
||||
'App Stacks',
|
||||
style: context.textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (appStacksState is StorageLoading && allStacks.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
if (appStacksState is StorageLoading && sortedStacks.isEmpty)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
childAspectRatio: 1.15,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => SkeletonizerConfig(
|
||||
data: AppColors.getSkeletonizerConfig(
|
||||
Theme.of(context).brightness,
|
||||
),
|
||||
child: const Skeletonizer(child: StackCardSkeleton()),
|
||||
),
|
||||
childCount: _kPageSize,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (sortedStacks.isEmpty)
|
||||
SliverFillRemaining(
|
||||
@@ -147,9 +166,16 @@ class AllStacksScreen extends HookConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.layers_outlined, size: 48, color: Colors.grey[400]),
|
||||
Icon(
|
||||
Icons.layers_outlined,
|
||||
size: 48,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('No stacks found', style: context.textTheme.titleMedium),
|
||||
Text(
|
||||
'No stacks found',
|
||||
style: context.textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -164,20 +190,17 @@ class AllStacksScreen extends HookConsumerWidget {
|
||||
mainAxisSpacing: 10,
|
||||
childAspectRatio: 1.15,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final stack = displayedStacks[index];
|
||||
final author = authorsMap[stack.event.pubkey];
|
||||
return StackCard(
|
||||
stack: stack,
|
||||
author: author,
|
||||
isAuthorLoading: isAuthorsLoading && author == null,
|
||||
previewIdentifiers: stackPreviewIds[stack.id] ?? [],
|
||||
appsMap: appsMap,
|
||||
);
|
||||
},
|
||||
childCount: displayedStacks.length,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final stack = displayedStacks[index];
|
||||
final author = authorsMap[stack.event.pubkey];
|
||||
return StackCard(
|
||||
stack: stack,
|
||||
author: author,
|
||||
isAuthorLoading: isAuthorsLoading && author == null,
|
||||
previewIdentifiers: stackPreviewIds[stack.id] ?? [],
|
||||
appsMap: appsMap,
|
||||
);
|
||||
}, childCount: displayedStacks.length),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
@@ -192,14 +215,15 @@ class AllStacksScreen extends HookConsumerWidget {
|
||||
),
|
||||
)
|
||||
: sortedStacks.isNotEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'No more stacks to load',
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(color: Colors.grey[600]),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
? Center(
|
||||
child: Text(
|
||||
'No more stacks to load',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 16)),
|
||||
@@ -232,12 +256,16 @@ List<AppStack> _sortStacks(
|
||||
others.sort((a, b) => b.event.createdAt.compareTo(a.event.createdAt));
|
||||
return [...followed, ...others];
|
||||
} else {
|
||||
final franzapStacks =
|
||||
stacks.where((s) => s.pubkey == kFranzapPubkey).toList();
|
||||
final otherStacks =
|
||||
stacks.where((s) => s.pubkey != kFranzapPubkey).toList();
|
||||
final franzapStacks = stacks
|
||||
.where((s) => s.pubkey == kFranzapPubkey)
|
||||
.toList();
|
||||
final otherStacks = stacks
|
||||
.where((s) => s.pubkey != kFranzapPubkey)
|
||||
.toList();
|
||||
|
||||
franzapStacks.sort((a, b) => b.event.createdAt.compareTo(a.event.createdAt));
|
||||
franzapStacks.sort(
|
||||
(a, b) => b.event.createdAt.compareTo(a.event.createdAt),
|
||||
);
|
||||
otherStacks.sort((a, b) => b.event.createdAt.compareTo(a.event.createdAt));
|
||||
return [...franzapStacks, ...otherStacks];
|
||||
}
|
||||
@@ -527,6 +527,7 @@ class _RepublishStacksButton extends HookConsumerWidget {
|
||||
identifier: stack.identifier,
|
||||
platform: platform,
|
||||
);
|
||||
partialStack.addCommunityKey(kZapstoreCommunityPubkey);
|
||||
|
||||
for (final appId in appIds) {
|
||||
partialStack.addApp(appId);
|
||||
@@ -536,7 +537,7 @@ class _RepublishStacksButton extends HookConsumerWidget {
|
||||
await ref.storage.save({signedStack});
|
||||
ref.storage.publish({
|
||||
signedStack,
|
||||
}, source: RemoteSource(relays: 'social'));
|
||||
}, source: RemoteSource(relays: {'social', 'AppCatalog'}));
|
||||
}
|
||||
|
||||
progressCount.value++;
|
||||
|
||||
@@ -90,13 +90,7 @@ class AppStackContainer extends HookConsumerWidget {
|
||||
final visibleCount = useState(_kInitialStacks);
|
||||
|
||||
if (showSkeleton) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_StackSectionHeader(showSeeAll: false),
|
||||
_buildSkeleton(context),
|
||||
],
|
||||
);
|
||||
return _buildSkeleton(context);
|
||||
}
|
||||
|
||||
final signedInPubkey = ref.watch(Signer.activePubkeyProvider);
|
||||
@@ -111,7 +105,7 @@ class AppStackContainer extends HookConsumerWidget {
|
||||
tags: {
|
||||
'#f': {platform},
|
||||
},
|
||||
source: const LocalAndRemoteSource(relays: 'social'),
|
||||
source: const LocalAndRemoteSource(relays: 'AppCatalog'),
|
||||
subscriptionPrefix: 'app-stack',
|
||||
schemaFilter: appStackEventFilter,
|
||||
),
|
||||
@@ -142,13 +136,7 @@ class AppStackContainer extends HookConsumerWidget {
|
||||
};
|
||||
|
||||
if (allStacks.isEmpty) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_StackSectionHeader(showSeeAll: false),
|
||||
_buildSkeleton(context),
|
||||
],
|
||||
);
|
||||
return _buildSkeleton(context);
|
||||
}
|
||||
|
||||
// Sort stacks
|
||||
@@ -229,60 +217,85 @@ class AppStackContainer extends HookConsumerWidget {
|
||||
}, [scrollController, sortedStacks.length]);
|
||||
|
||||
// 2-row horizontal scroll layout
|
||||
final numColumns = (displayedStacks.length + 1) ~/ 2;
|
||||
// Add a "See more" card after the last stack if there are more stacks
|
||||
final showSeeMore = sortedStacks.length > _kInitialStacks;
|
||||
// Total items = displayed stacks + optional "See more" placeholder
|
||||
final totalItems = displayedStacks.length + (showSeeMore ? 1 : 0);
|
||||
final numColumns = (totalItems + 1) ~/ 2;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_StackSectionHeader(showSeeAll: sortedStacks.length > _kInitialStacks),
|
||||
SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.only(left: 12, right: 12),
|
||||
clipBehavior: Clip.none,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (int col = 0; col < numColumns; col++)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: SizedBox(
|
||||
width: 160,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (col * 2 < displayedStacks.length)
|
||||
StackCard(
|
||||
stack: displayedStacks[col * 2],
|
||||
author: authorsMap[displayedStacks[col * 2].event.pubkey],
|
||||
isAuthorLoading: isAuthorLoading(
|
||||
displayedStacks[col * 2].event.pubkey,
|
||||
),
|
||||
previewIdentifiers:
|
||||
stackPreviewIds[displayedStacks[col * 2].id] ?? [],
|
||||
appsMap: appsMap,
|
||||
),
|
||||
if (col * 2 + 1 < displayedStacks.length) ...[
|
||||
const SizedBox(height: 10),
|
||||
StackCard(
|
||||
stack: displayedStacks[col * 2 + 1],
|
||||
author: authorsMap[displayedStacks[col * 2 + 1].event.pubkey],
|
||||
isAuthorLoading: isAuthorLoading(
|
||||
displayedStacks[col * 2 + 1].event.pubkey,
|
||||
),
|
||||
previewIdentifiers:
|
||||
stackPreviewIds[displayedStacks[col * 2 + 1].id] ?? [],
|
||||
appsMap: appsMap,
|
||||
),
|
||||
],
|
||||
],
|
||||
return SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.only(left: 12, right: 12),
|
||||
clipBehavior: Clip.none,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (int col = 0; col < numColumns; col++)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: SizedBox(
|
||||
width: 160,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildItemAtIndex(
|
||||
context,
|
||||
col * 2,
|
||||
displayedStacks,
|
||||
authorsMap,
|
||||
isAuthorLoading,
|
||||
stackPreviewIds,
|
||||
appsMap,
|
||||
showSeeMore,
|
||||
totalItems,
|
||||
),
|
||||
),
|
||||
if (col * 2 + 1 < totalItems) ...[
|
||||
const SizedBox(height: 10),
|
||||
_buildItemAtIndex(
|
||||
context,
|
||||
col * 2 + 1,
|
||||
displayedStacks,
|
||||
authorsMap,
|
||||
isAuthorLoading,
|
||||
stackPreviewIds,
|
||||
appsMap,
|
||||
showSeeMore,
|
||||
totalItems,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemAtIndex(
|
||||
BuildContext context,
|
||||
int index,
|
||||
List<AppStack> displayedStacks,
|
||||
Map<String, Profile> authorsMap,
|
||||
bool Function(String) isAuthorLoading,
|
||||
Map<String, List<String>> stackPreviewIds,
|
||||
Map<String, App> appsMap,
|
||||
bool showSeeMore,
|
||||
int totalItems,
|
||||
) {
|
||||
final isSeeMore = showSeeMore && index == totalItems - 1;
|
||||
if (isSeeMore) {
|
||||
return _SeeMoreCard();
|
||||
}
|
||||
if (index >= displayedStacks.length) return const SizedBox.shrink();
|
||||
final stack = displayedStacks[index];
|
||||
return StackCard(
|
||||
stack: stack,
|
||||
author: authorsMap[stack.event.pubkey],
|
||||
isAuthorLoading: isAuthorLoading(stack.event.pubkey),
|
||||
previewIdentifiers: stackPreviewIds[stack.id] ?? [],
|
||||
appsMap: appsMap,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -304,9 +317,9 @@ class AppStackContainer extends HookConsumerWidget {
|
||||
child: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_SkeletonStackCard(),
|
||||
StackCardSkeleton(),
|
||||
SizedBox(height: 10),
|
||||
_SkeletonStackCard(),
|
||||
StackCardSkeleton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -319,47 +332,42 @@ class AppStackContainer extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Section header with title and optional "See all" button
|
||||
class _StackSectionHeader extends StatelessWidget {
|
||||
const _StackSectionHeader({required this.showSeeAll});
|
||||
|
||||
final bool showSeeAll;
|
||||
|
||||
/// "See more" card that navigates to AllStacksScreen
|
||||
class _SeeMoreCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 0, 6, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Text('App Stacks', style: context.textTheme.titleLarge),
|
||||
const Spacer(),
|
||||
if (showSeeAll)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
final segments = GoRouterState.of(context).uri.pathSegments;
|
||||
final first = segments.isNotEmpty ? segments.first : 'search';
|
||||
context.push('/$first/stacks');
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
child: Text(
|
||||
'See all',
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final segments = GoRouterState.of(context).uri.pathSegments;
|
||||
final first = segments.isNotEmpty ? segments.first : 'search';
|
||||
context.push('/$first/stacks');
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'See more',
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Skeleton card for loading state
|
||||
class _SkeletonStackCard extends StatelessWidget {
|
||||
const _SkeletonStackCard();
|
||||
class StackCardSkeleton extends StatelessWidget {
|
||||
const StackCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -638,6 +638,7 @@ class _AddToStackDialogSignedIn extends HookConsumerWidget {
|
||||
identifier: collectionId,
|
||||
platform: platform,
|
||||
);
|
||||
partialStack.addCommunityKey(kZapstoreCommunityPubkey);
|
||||
|
||||
// Add existing apps in order
|
||||
for (final appId in existingAppIds) {
|
||||
@@ -651,7 +652,7 @@ class _AddToStackDialogSignedIn extends HookConsumerWidget {
|
||||
await ref.storage.save({signedStack});
|
||||
await ref.storage.publish({
|
||||
signedStack,
|
||||
}, source: RemoteSource(relays: 'social'));
|
||||
}, source: RemoteSource(relays: {'social', 'AppCatalog'}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,6 +667,7 @@ class _AddToStackDialogSignedIn extends HookConsumerWidget {
|
||||
identifier: collectionId,
|
||||
platform: platform,
|
||||
);
|
||||
partialStack.addCommunityKey(kZapstoreCommunityPubkey);
|
||||
|
||||
// Re-add all apps except the one we're removing, preserving order
|
||||
// Get raw app IDs from event tags as a list to preserve order
|
||||
@@ -682,7 +684,7 @@ class _AddToStackDialogSignedIn extends HookConsumerWidget {
|
||||
await ref.storage.save({signedStack});
|
||||
await ref.storage.publish({
|
||||
signedStack,
|
||||
}, source: RemoteSource(relays: 'social'));
|
||||
}, source: RemoteSource(relays: {'social', 'AppCatalog'}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,11 @@ If any invariant is violated, the implementation is incorrect.
|
||||
- Parsing unknown, missing, or future tags must not crash the app.
|
||||
- Partial or invalid data must degrade gracefully.
|
||||
|
||||
## App Stack (kind 30267) Tag Rules
|
||||
|
||||
- Encrypted stacks (private collections, e.g. "Saved Apps") MUST NOT include a community `p` tag or `h` tag.
|
||||
- Only public stacks may carry a community `p` tag to associate them with a community.
|
||||
|
||||
## Lifecycle Safety
|
||||
|
||||
- Subscriptions must always be cancellable.
|
||||
|
||||
Reference in New Issue
Block a user