diff --git a/lib/app/features/feed/views/components/community_token_live/components/feed_profile_token.dart b/lib/app/features/feed/views/components/community_token_live/components/feed_profile_token.dart index 5b2aa33fd6..0456239a8a 100644 --- a/lib/app/features/feed/views/components/community_token_live/components/feed_profile_token.dart +++ b/lib/app/features/feed/views/components/community_token_live/components/feed_profile_token.dart @@ -82,7 +82,6 @@ class ProfileTokenHeader extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final creatorPubkey = MasterPubkeyResolver.resolve( externalAddress, - eventReference: null, ); final isCurrentUserProfile = ref.watch(isCurrentUserSelectorProvider(creatorPubkey)); diff --git a/lib/app/features/tokenized_communities/providers/token_market_info_provider.r.dart b/lib/app/features/tokenized_communities/providers/token_market_info_provider.r.dart index c2b4ba1d29..4ad04d7443 100644 --- a/lib/app/features/tokenized_communities/providers/token_market_info_provider.r.dart +++ b/lib/app/features/tokenized_communities/providers/token_market_info_provider.r.dart @@ -37,7 +37,7 @@ Stream tokenMarketInfo( ); } - yield currentToken; + yield currentToken ?? cachedToken; // 2. Subscribe to real-time updates final subscription = await client.communityTokens.subscribeToTokenInfo(externalAddress); diff --git a/lib/app/features/tokenized_communities/services/token_operation_protected_accounts_service.dart b/lib/app/features/tokenized_communities/services/token_operation_protected_accounts_service.dart index f574a8c17a..0aedc20ebb 100644 --- a/lib/app/features/tokenized_communities/services/token_operation_protected_accounts_service.dart +++ b/lib/app/features/tokenized_communities/services/token_operation_protected_accounts_service.dart @@ -26,11 +26,15 @@ class TokenOperationProtectedAccountsService { } // Checks if the account associated with the given external address is protected from token operations. + // for x tokens external address has not a master pubkey so catch exception and return false bool isProtectedAccountFromExternalAddress(String externalAddress) { - final masterPubkey = MasterPubkeyResolver.resolve( - externalAddress, - eventReference: null, - ); - return isProtectedAccount(masterPubkey); + try { + final masterPubkey = MasterPubkeyResolver.resolve( + externalAddress, + ); + return isProtectedAccount(masterPubkey); + } catch (_) { + return false; + } } } diff --git a/lib/app/features/tokenized_communities/utils/master_pubkey_resolver.dart b/lib/app/features/tokenized_communities/utils/master_pubkey_resolver.dart index 13c0cd12b7..93f868b00d 100644 --- a/lib/app/features/tokenized_communities/utils/master_pubkey_resolver.dart +++ b/lib/app/features/tokenized_communities/utils/master_pubkey_resolver.dart @@ -5,7 +5,7 @@ import 'package:ion/app/features/ion_connect/model/event_reference.f.dart'; class MasterPubkeyResolver { MasterPubkeyResolver._(); - static String resolve(String externalAddress, {required EventReference? eventReference}) { + static String resolve(String externalAddress, {EventReference? eventReference}) { return eventReference?.masterPubkey ?? ReplaceableEventReference.fromString(externalAddress).masterPubkey; } diff --git a/lib/app/features/user/pages/creator_tokens/views/creator_tokens_page/components/creator_tokens_header.dart b/lib/app/features/user/pages/creator_tokens/views/creator_tokens_page/components/creator_tokens_header.dart index 8aefaf4ac9..7ffeac8305 100644 --- a/lib/app/features/user/pages/creator_tokens/views/creator_tokens_page/components/creator_tokens_header.dart +++ b/lib/app/features/user/pages/creator_tokens/views/creator_tokens_page/components/creator_tokens_header.dart @@ -25,6 +25,7 @@ class CreatorTokensHeader extends ConsumerWidget { required this.backButtonIcon, required this.onPop, required this.onSearchToggle, + this.carouselKey, this.scrollController, super.key, }); @@ -38,6 +39,7 @@ class CreatorTokensHeader extends ConsumerWidget { final Widget backButtonIcon; final VoidCallback onPop; final VoidCallback onSearchToggle; + final Key? carouselKey; final ScrollController? scrollController; @override @@ -65,19 +67,24 @@ class CreatorTokensHeader extends ConsumerWidget { ), Opacity( opacity: 1 - opacity, - child: featuredTokensAsync.when( - data: (tokens) { - if (tokens.isEmpty) return const SizedBox.shrink(); + child: SizedBox( + key: carouselKey, + width: double.infinity, + height: CreatorTokensCarousel.carouselHeight.s, + child: featuredTokensAsync.when( + data: (tokens) { + if (tokens.isEmpty) return const SizedBox.shrink(); - return CreatorTokensCarousel( - tokens: tokens, - onItemChanged: (token) { - selectedToken.value = token; - }, - ); - }, - loading: () => const CreatorTokensCarouselSkeleton(), - error: (_, __) => const SizedBox.shrink(), + return CreatorTokensCarousel( + tokens: tokens, + onItemChanged: (token) { + selectedToken.value = token; + }, + ); + }, + loading: () => const CreatorTokensCarouselSkeleton(), + error: (_, __) => const SizedBox.shrink(), + ), ), ), ], diff --git a/lib/app/features/user/pages/creator_tokens/views/creator_tokens_page/creator_tokens_page.dart b/lib/app/features/user/pages/creator_tokens/views/creator_tokens_page/creator_tokens_page.dart index 6ae8d957bd..de623c65cd 100644 --- a/lib/app/features/user/pages/creator_tokens/views/creator_tokens_page/creator_tokens_page.dart +++ b/lib/app/features/user/pages/creator_tokens/views/creator_tokens_page/creator_tokens_page.dart @@ -23,6 +23,7 @@ import 'package:ion/app/hooks/use_avatar_colors.dart'; import 'package:ion/app/hooks/use_on_init.dart'; import 'package:ion/app/router/components/navigation_app_bar/navigation_app_bar.dart'; import 'package:ion/app/router/components/navigation_app_bar/navigation_back_button.dart'; +import 'package:ion/app/router/utils/back_gesture_exclusion.dart'; import 'package:ion/generated/assets.gen.dart'; import 'package:ion_token_analytics/ion_token_analytics.dart'; @@ -64,6 +65,60 @@ class CreatorTokensPage extends HookConsumerWidget { final isGlobalSearchVisible = useState(false); final lastSearchQuery = useRef(null); + final carouselKey = useMemoized(GlobalKey.new); + final carouselRect = useMemoized(() => ValueNotifier(null)); + final route = ModalRoute.of(context); + + void updateCarouselRect() { + final carouselContext = carouselKey.currentContext; + if (carouselContext == null) { + if (carouselRect.value != null) { + carouselRect.value = null; + } + return; + } + + final renderObject = carouselContext.findRenderObject(); + if (renderObject is! RenderBox || !renderObject.hasSize) { + return; + } + + final origin = renderObject.localToGlobal(Offset.zero); + final rect = origin & renderObject.size; + if (carouselRect.value != rect) { + carouselRect.value = rect; + } + } + + useEffect( + () { + if (route == null) { + return null; + } + + BackGestureExclusionRegistry.register(route, carouselRect); + return () => BackGestureExclusionRegistry.unregister(route, carouselRect); + }, + [route, carouselRect], + ); + + useEffect( + () { + void listener() => updateCarouselRect(); + scrollController.addListener(listener); + WidgetsBinding.instance.addPostFrameCallback((_) => updateCarouselRect()); + return () => scrollController.removeListener(listener); + }, + [scrollController], + ); + + useEffect( + () { + return carouselRect.dispose; + }, + const [], + ); + // Collapse header when search field is focused useOnInit( () { @@ -200,6 +255,7 @@ class CreatorTokensPage extends HookConsumerWidget { resetGlobalSearch(); } }, + carouselKey: carouselKey, ), const SliverToBoxAdapter( child: SectionSeparator(), diff --git a/lib/app/router/custom_routes.dart b/lib/app/router/custom_routes.dart index 470cf8bc53..927b132a3c 100644 --- a/lib/app/router/custom_routes.dart +++ b/lib/app/router/custom_routes.dart @@ -16,6 +16,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:ion/app/extensions/extensions.dart'; +import 'package:ion/app/router/utils/back_gesture_exclusion.dart'; const double _kMinFlingVelocity = 1; // Screen widths per second. @@ -238,6 +239,8 @@ mixin CupertinoRouteTransitionMixin on PageRoute { linearTransition: linearTransition, child: _CupertinoBackGestureDetector( enabledCallback: () => route.popGestureEnabled, + shouldStartPopGesture: (event) => + !BackGestureExclusionRegistry.isExcluded(route, event.position), onStartPopGesture: () => _startPopGesture(route), child: child, ), @@ -687,6 +690,7 @@ class _CupertinoBackGestureDetector extends StatefulWidget { required this.enabledCallback, required this.onStartPopGesture, required this.child, + this.shouldStartPopGesture, super.key, }); @@ -694,6 +698,8 @@ class _CupertinoBackGestureDetector extends StatefulWidget { final ValueGetter enabledCallback; + final bool Function(PointerDownEvent event)? shouldStartPopGesture; + final ValueGetter<_CupertinoBackGestureController> onStartPopGesture; @override @@ -761,9 +767,15 @@ class _CupertinoBackGestureDetectorState extends State<_CupertinoBackGestureD } void _handlePointerDown(PointerDownEvent event) { - if (widget.enabledCallback()) { - _recognizer.addPointer(event); + if (!widget.enabledCallback()) { + return; + } + + if (widget.shouldStartPopGesture != null && !widget.shouldStartPopGesture!(event)) { + return; } + + _recognizer.addPointer(event); } double _convertToLogical(double value) { diff --git a/lib/app/router/profile_routes.dart b/lib/app/router/profile_routes.dart index 78988f2582..d644e2efd1 100644 --- a/lib/app/router/profile_routes.dart +++ b/lib/app/router/profile_routes.dart @@ -410,6 +410,7 @@ class CreatorTokensRoute extends BaseRouteData with _$CreatorTokensRoute { CreatorTokensRoute() : super( child: const CreatorTokensPage(), + canPop: true, ); } diff --git a/lib/app/router/utils/back_gesture_exclusion.dart b/lib/app/router/utils/back_gesture_exclusion.dart new file mode 100644 index 0000000000..3bcbeeca32 --- /dev/null +++ b/lib/app/router/utils/back_gesture_exclusion.dart @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: ice License 1.0 + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +class BackGestureExclusionRegistry { + static final Map, Set>> _entries = {}; + + static void register(Route route, ValueListenable rectListenable) { + _entries.putIfAbsent(route, () => >{}).add(rectListenable); + } + + static void unregister(Route route, ValueListenable rectListenable) { + final entries = _entries[route]; + if (entries == null) { + return; + } + + entries.remove(rectListenable); + if (entries.isEmpty) { + _entries.remove(route); + } + } + + static bool isExcluded(Route route, Offset globalPosition) { + final entries = _entries[route]; + if (entries == null) { + return false; + } + + for (final entry in entries) { + final rect = entry.value; + if (rect != null && rect.contains(globalPosition)) { + return true; + } + } + + return false; + } +}