Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions mobile/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Handle buzz:// deep links (e.g. buzz://message?channel=…&id=…). -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="buzz"/>
</intent-filter>
<!-- Disable Flutter's built-in deeplink handler; app_links owns
incoming URLs so they can be routed with app state in Dart. -->
<meta-data
android:name="flutter_deeplinking_enabled"
android:value="false" />
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
Expand Down
15 changes: 15 additions & 0 deletions mobile/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,23 @@
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.buzz.deeplink</string>
<key>CFBundleURLSchemes</key>
<array>
<string>buzz</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>FlutterDeepLinkingEnabled</key>
<false/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
Expand Down
10 changes: 9 additions & 1 deletion mobile/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import 'features/channels/unread_badge/unread_badge_provider.dart';
import 'features/home/home_page.dart';
import 'features/pairing/pairing_page.dart';
import 'features/channels/agent_activity/observer_subscription.dart';
import 'features/channels/deep_link_dispatcher.dart';
import 'features/profile/user_status_cache_provider.dart';
import 'shared/auth/auth.dart';
import 'shared/deeplink/pending_deep_link_provider.dart';
import 'shared/relay/relay.dart';
import 'shared/theme/theme.dart';

Expand Down Expand Up @@ -39,6 +41,10 @@ class App extends HookConsumerWidget {
ref.watch(userStatusCacheProvider);
}

// Start listening for buzz:// links immediately (even pre-auth) so a
// cold-start link survives until the authenticated UI can dispatch it.
ref.watch(pendingDeepLinkProvider);

void applyBadge(UnreadBadgeState state) {
if (state.highPriorityCount > 0) {
AppBadgePlus.updateBadge(state.highPriorityCount);
Expand Down Expand Up @@ -66,7 +72,9 @@ class App extends HookConsumerWidget {
loading: () => const _SplashScreen(),
error: (_, _) => const PairingPage(),
data: (state) => switch (state.status) {
AuthStatus.authenticated => const HomePage(),
AuthStatus.authenticated => const DeepLinkDispatcher(
child: HomePage(),
),
_ => const PairingPage(),
},
),
Expand Down
36 changes: 35 additions & 1 deletion mobile/lib/features/channels/channel_detail_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';

import '../../shared/relay/relay.dart';
import '../../shared/theme/theme.dart';
Expand Down Expand Up @@ -47,6 +48,21 @@ part 'channel_detail_page/message_bubble.dart';
part 'channel_detail_page/banners.dart';
part 'channel_detail_page/app_bar.dart';

/// Fetch deep-link targets that may be outside the loaded channel window.
Future<void> _loadDeepLinkEvents(
WidgetRef ref,
String channelId,
Set<String> eventIds,
) async {
try {
await ref
.read(channelMessagesProvider(channelId).notifier)
.loadEventsById(eventIds);
} catch (error) {
debugPrint('deep-link: failed to load target messages: $error');
}
}

/// Fetch channel members and preload their profiles into the user cache.
Future<void> _preloadMembers(WidgetRef ref, String channelId) async {
// Capture references before async gap to avoid using disposed ref.
Expand Down Expand Up @@ -89,8 +105,15 @@ int? _channelReadTimestamp({

class ChannelDetailPage extends HookConsumerWidget {
final Channel channel;
final String? initialMessageId;
final String? initialThreadRootId;

const ChannelDetailPage({super.key, required this.channel});
const ChannelDetailPage({
super.key,
required this.channel,
this.initialMessageId,
this.initialThreadRootId,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
Expand Down Expand Up @@ -135,6 +158,15 @@ class ChannelDetailPage extends HookConsumerWidget {
return null;
}, [channel.id]);

useEffect(() {
final messageId = initialMessageId;
if (messageId == null || channel.isForum) return null;
final eventIds = {messageId, ?initialThreadRootId};
final notifier = ref.read(channelMessagesProvider(channel.id).notifier);
unawaited(_loadDeepLinkEvents(ref, channel.id, eventIds));
return () => notifier.releaseDeepLinkEvents(eventIds);
}, [channel.id, initialMessageId, initialThreadRootId]);

useEffect(() {
if (!readState.isReady || readTimestamp == null) {
return null;
Expand Down Expand Up @@ -274,6 +306,8 @@ class ChannelDetailPage extends HookConsumerWidget {
return _MessageList(
entries: entries,
allMessages: messages,
initialMessageId: initialMessageId,
initialThreadRootId: initialThreadRootId,
channelId: channel.id,
currentPubkey: currentPubkey,
isMember: resolvedChannel.isMember,
Expand Down
Loading
Loading