import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../shared/deeplink/deep_link.dart'; import '../../shared/deeplink/pending_deep_link_provider.dart'; import '../../shared/l10n/l10n.dart'; import '../invites/invite_join_provider.dart'; import '../invites/invite_join_sheet.dart'; import 'channel.dart'; import 'channel_detail_page.dart'; import 'channels_provider.dart'; /// Routes pending `buzz://message` deep links into the channel view. /// /// Wraps the authenticated home subtree. Whenever a parsed link is parked in /// [pendingDeepLinkProvider] and the channel list is available, this pushes /// the target [ChannelDetailPage] on the enclosing [Navigator]. Links are /// held (not dropped) while channels are still loading, so cold-start links /// dispatch as soon as the first channel fetch completes. typedef DeepLinkDestinationBuilder = Widget Function(Channel channel, MessageDeepLink link); class DeepLinkDispatcher extends ConsumerStatefulWidget { final Widget child; final DeepLinkDestinationBuilder? destinationBuilder; final bool dispatchMessageLinks; const DeepLinkDispatcher({ super.key, required this.child, this.destinationBuilder, this.dispatchMessageLinks = true, }); @override ConsumerState createState() => _DeepLinkDispatcherState(); } class _DeepLinkDispatcherState extends ConsumerState { bool _preparingInvite = false; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; _maybeDispatch(ref.read(pendingDeepLinkProvider)); }); } @override Widget build(BuildContext context) { // Re-evaluate dispatch when either a new link arrives or channels load. ref.listen(pendingDeepLinkProvider, (_, link) { _maybeDispatch(link); }); if (widget.dispatchMessageLinks) { ref.listen>>(channelsProvider, (_, _) { _maybeDispatch(ref.read(pendingDeepLinkProvider)); }); } return widget.child; } void _maybeDispatch(BuzzDeepLink? link) { if (link == null) return; if (link is InviteDeepLink) { _maybeDispatchInvite(link); return; } if (link is! MessageDeepLink || !widget.dispatchMessageLinks) return; final channels = ref.read(channelsProvider).asData?.value; // Channels not loaded yet — keep the link parked; the channelsProvider // listener re-attempts once data arrives. if (channels == null) return; ref.read(pendingDeepLinkProvider.notifier).consume(); final channel = channels .where((c) => c.id == link.channelId) .cast() .firstOrNull; if (channel == null) { debugPrint( 'deep-link: channel ${link.channelId} not found in workspace; ' 'dropping link', ); ScaffoldMessenger.maybeOf(context)?.showSnackBar( SnackBar(content: Text(context.l10n.channelNotFound)), ); return; } if (!context.mounted) return; Navigator.of(context).push( MaterialPageRoute( builder: (_) => widget.destinationBuilder?.call(channel, link) ?? ChannelDetailPage( channel: channel, initialMessageId: link.messageId, initialThreadRootId: link.threadRootId, ), ), ); } void _maybeDispatchInvite(InviteDeepLink link) { if (_preparingInvite) return; _preparingInvite = true; final navigatorContext = context; final messenger = ScaffoldMessenger.maybeOf(context); Future.microtask(() async { try { await ref.read(inviteJoinProvider.notifier).prepare(link); ref.read(pendingDeepLinkProvider.notifier).consume(); if (!navigatorContext.mounted) return; final status = ref.read(inviteJoinProvider).status; if (status == InviteJoinStatus.confirming) { showInviteJoinSheet(navigatorContext, ref); } else if (status == InviteJoinStatus.switchedExisting) { messenger?.showSnackBar( SnackBar(content: Text(navigatorContext.l10n.switchedCommunity)), ); } } catch (error) { debugPrint('deep-link: failed to prepare invite: $error'); if (navigatorContext.mounted) { messenger?.showSnackBar( SnackBar(content: Text(navigatorContext.l10n.inviteOpenFailed)), ); } } finally { _preparingInvite = false; } }); } }