fix(mobile): open Search and profile DMs as ordinary routes

Search and the profile sheet pushed the opened DM with the default lookup
placement, so while Solo groups loaded the route showed only a spinner
for up to 2s. Pass the ordinary placement, as New message already does.
Known Solo placement still takes over once groups load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-28 06:38:55 -07:00
parent d7329aecd2
commit 1fe2e35fc1
4 changed files with 203 additions and 2 deletions

View File

@@ -49,7 +49,10 @@ void showUserProfileSheet(
if (channel == null || !context.mounted) return; if (channel == null || !context.mounted) return;
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute<void>( MaterialPageRoute<void>(
builder: (_) => ChannelDetailPage(channel: channel), builder: (_) => ChannelDetailPage(
channel: channel,
placement: ChannelRoutePlacement.ordinary,
),
), ),
); );
}); });

View File

@@ -587,7 +587,10 @@ class _PeopleSection extends ConsumerWidget {
if (!context.mounted) return; if (!context.mounted) return;
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute<void>( MaterialPageRoute<void>(
builder: (_) => ChannelDetailPage(channel: channel), builder: (_) => ChannelDetailPage(
channel: channel,
placement: ChannelRoutePlacement.ordinary,
),
), ),
); );
}, },

View File

@@ -0,0 +1,111 @@
import 'package:buzz/features/channels/channel.dart';
import 'package:buzz/features/channels/channel_detail_page.dart';
import 'package:buzz/features/channels/channel_management_provider.dart';
import 'package:buzz/features/channels/compose_bar.dart';
import 'package:buzz/features/profile/user_profile_sheet.dart';
import 'package:buzz/shared/me/me_sync.dart';
import 'package:buzz/shared/profile/user_cache_provider.dart';
import 'package:buzz/shared/profile/user_profile.dart';
import 'package:buzz/shared/theme/theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
final peer = 'a' * 64;
testWidgets('Message opens the DM without waiting on Solo', (tester) async {
final dm = Channel(
id: 'dm-peer',
name: 'Peer',
channelType: 'dm',
visibility: 'private',
description: '',
memberCount: 2,
createdBy: 'self',
createdAt: DateTime(2025),
isMember: true,
);
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final opened = <List<String>>[];
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
ProviderScope(
overrides: [
userCacheProvider.overrideWith(
() => _UserCache({
peer: UserProfile(pubkey: peer, displayName: 'Peer'),
}),
),
channelActionsProvider.overrideWith(
(ref) => _FakeDmActions(dm, opened),
),
savedPrefsProvider.overrideWithValue(prefs),
// Solo organization is still loading.
meGroupsProvider.overrideWith((ref) => const Stream.empty()),
],
child: MaterialApp(
theme: AppTheme.light(),
home: Builder(
builder: (context) => Scaffold(
body: TextButton(
onPressed: () => showUserProfileSheet(context, peer),
child: const Text('Open profile'),
),
),
),
),
),
);
await tester.tap(find.text('Open profile'));
await tester.pumpAndSettle();
await tester.tap(find.text('Message'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(opened, [
[peer],
]);
expect(
tester.widget<ChannelDetailPage>(find.byType(ChannelDetailPage)).channel,
dm,
);
// The DM is usable at once, not a placement spinner.
expect(find.byType(ComposeBar), findsOneWidget);
});
}
class _UserCache extends UserCacheNotifier {
_UserCache(this._initial);
final Map<String, UserProfile> _initial;
@override
Map<String, UserProfile> build() => _initial;
@override
Future<bool> preload(List<String> pubkeys) async => true;
}
class _FakeDmActions implements ChannelActions {
_FakeDmActions(this.dm, this.opened);
final Channel dm;
final List<List<String>> opened;
@override
Future<Channel> openDm({
required List<String> pubkeys,
bool waitForChannelList = true,
}) async {
opened.add(pubkeys);
return dm;
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}

View File

@@ -1,6 +1,8 @@
import 'package:buzz/shared/widgets/buzz_agent_icon.dart'; import 'package:buzz/shared/widgets/buzz_agent_icon.dart';
import 'package:buzz/features/channels/channel.dart'; import 'package:buzz/features/channels/channel.dart';
import 'package:buzz/features/channels/channel_detail_page.dart';
import 'package:buzz/features/channels/channel_management_provider.dart'; import 'package:buzz/features/channels/channel_management_provider.dart';
import 'package:buzz/features/channels/compose_bar.dart';
import 'package:buzz/features/channels/channels_provider.dart'; import 'package:buzz/features/channels/channels_provider.dart';
import 'package:buzz/features/channels/message_content.dart'; import 'package:buzz/features/channels/message_content.dart';
import 'package:buzz/features/channels/small_avatar.dart'; import 'package:buzz/features/channels/small_avatar.dart';
@@ -12,12 +14,14 @@ import 'package:buzz/features/search/search_page.dart';
import 'package:buzz/features/search/search_provider.dart'; import 'package:buzz/features/search/search_provider.dart';
import 'package:buzz/shared/theme/theme.dart'; import 'package:buzz/shared/theme/theme.dart';
import 'package:buzz/shared/mentions/agent_identity_provider.dart'; import 'package:buzz/shared/mentions/agent_identity_provider.dart';
import 'package:buzz/shared/me/me_sync.dart';
import 'package:buzz/shared/widgets/concentric_sheet_surface.dart'; import 'package:buzz/shared/widgets/concentric_sheet_surface.dart';
import 'package:buzz/shared/widgets/ios_glass_navigation_button.dart'; import 'package:buzz/shared/widgets/ios_glass_navigation_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../helpers/widget_helpers.dart'; import '../../helpers/widget_helpers.dart';
@@ -112,6 +116,67 @@ void main() {
} }
} }
testWidgets('a person result opens their DM without waiting on Solo', (
tester,
) async {
final dm = Channel(
id: 'dm-maya',
name: 'Maya',
channelType: 'dm',
visibility: 'private',
description: '',
memberCount: 2,
createdBy: 'test',
createdAt: DateTime(2025),
isMember: true,
);
final opened = <List<String>>[];
SharedPreferences.setMockInitialValues({});
final prefs = await SharedPreferences.getInstance();
await tester.pumpWidget(
WidgetHelpers.testable(
overrides: [
searchProvider.overrideWith(
() => _FakeSearchNotifier(
const SearchState(
query: 'maya',
userResults: [
DirectoryUser(pubkey: 'maya', displayName: 'Maya'),
],
),
),
),
recentSearchesProvider.overrideWith(
() => _FakeRecentSearchesNotifier(const []),
),
profileProvider.overrideWith(() => _FakeProfileNotifier()),
channelsProvider.overrideWith(() => _FakeChannelsNotifier()),
channelActionsProvider.overrideWith(
(ref) => _FakeDmActions(dm, opened),
),
savedPrefsProvider.overrideWithValue(prefs),
// Solo organization is still loading.
meGroupsProvider.overrideWith((ref) => const Stream.empty()),
],
child: const SearchPage(),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const ValueKey('search-person-row-maya')));
await tester.pump();
await tester.pump();
expect(opened, [
['maya'],
]);
final page = tester.widget<ChannelDetailPage>(
find.byType(ChannelDetailPage),
);
expect(page.channel.id, dm.id);
// The DM is usable at once, not a placement spinner.
expect(find.byType(ComposeBar), findsOneWidget);
});
testWidgets('iOS native filter press selects Channels and filters results', ( testWidgets('iOS native filter press selects Channels and filters results', (
tester, tester,
) async { ) async {
@@ -933,6 +998,25 @@ class _FakeChannelsNotifier extends ChannelsNotifier {
Future<List<Channel>> build() async => channels; Future<List<Channel>> build() async => channels;
} }
class _FakeDmActions implements ChannelActions {
_FakeDmActions(this.dm, this.opened);
final Channel dm;
final List<List<String>> opened;
@override
Future<Channel> openDm({
required List<String> pubkeys,
bool waitForChannelList = true,
}) async {
opened.add(pubkeys);
return dm;
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeUserCacheNotifier extends UserCacheNotifier { class _FakeUserCacheNotifier extends UserCacheNotifier {
_FakeUserCacheNotifier(this.profile); _FakeUserCacheNotifier(this.profile);