Files
buzz/mobile/lib/features/channels/deep_link_dispatcher.dart
Tyler 648cbf3610 feat: add invite QR and mobile direct join (#1957)
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1jmc9dt2lyvzu3h0kxlwxt5zg4fxp9476awyxw6gwxn72g6cw7exqs64whm <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
2026-07-16 13:40:24 -07:00

144 lines
4.6 KiB
Dart

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 '../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<DeepLinkDispatcher> createState() => _DeepLinkDispatcherState();
}
class _DeepLinkDispatcherState extends ConsumerState<DeepLinkDispatcher> {
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<BuzzDeepLink?>(pendingDeepLinkProvider, (_, link) {
_maybeDispatch(link);
});
if (widget.dispatchMessageLinks) {
ref.listen<AsyncValue<List<Channel>>>(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<Channel?>()
.firstOrNull;
if (channel == null) {
debugPrint(
'deep-link: channel ${link.channelId} not found in workspace; '
'dropping link',
);
ScaffoldMessenger.maybeOf(context)?.showSnackBar(
const SnackBar(content: Text('Channel not found in this workspace')),
);
return;
}
if (!context.mounted) return;
Navigator.of(context).push(
MaterialPageRoute<void>(
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(
const SnackBar(content: Text('Switched to this community')),
);
}
} catch (error) {
debugPrint('deep-link: failed to prepare invite: $error');
if (navigatorContext.mounted) {
messenger?.showSnackBar(
const SnackBar(
content: Text(
'Could not open this invite. Re-open the invite link to try again.',
),
),
);
}
} finally {
_preparingInvite = false;
}
});
}
}