diff --git a/mobile/lib/features/onboarding/hosted_community_selection.dart b/mobile/lib/features/onboarding/hosted_community_selection.dart index 094c038921..db29411225 100644 --- a/mobile/lib/features/onboarding/hosted_community_selection.dart +++ b/mobile/lib/features/onboarding/hosted_community_selection.dart @@ -3,7 +3,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../shared/hosted/hosted_flow.dart'; -import '../../shared/hosted/hosted_environment.dart'; import '../../shared/theme/grid.dart'; import '../../shared/community/community_grid.dart'; import '../../shared/community/community_grid_option.dart'; @@ -75,10 +74,7 @@ class HostedCommunitySelection extends HookConsumerWidget { for (final community in flow.communities) CommunityGridOption( id: community.relayUrl, - name: HostedEnvironment.communityDisplayName( - community.name, - community.relayUrl, - ), + name: community.name, relayUrl: community.relayUrl, onSelect: (_) async { flow.chooseLandingCommunity(community.relayUrl); diff --git a/mobile/lib/shared/community/community.dart b/mobile/lib/shared/community/community.dart index 96c2b5f7a2..a992c28228 100644 --- a/mobile/lib/shared/community/community.dart +++ b/mobile/lib/shared/community/community.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:uuid/uuid.dart'; +import '../hosted/hosted_environment.dart'; import '../push/push_subscription.dart'; const _uuid = Uuid(); @@ -24,7 +25,14 @@ class Community { static const _pushNotificationsEnabledKey = 'pushNotificationsEnabledV2'; final String id; - final String name; + + /// Saved label, which may be a hosted relay's full host name. + final String _name; + + /// User-facing label. Every reader goes through here so hosted host-name + /// fallbacks always appear in the short form onboarding shows. + String get name => HostedEnvironment.communityDisplayName(_name, relayUrl); + final String relayUrl; final String? pubkey; final String? nsec; @@ -44,7 +52,7 @@ class Community { const Community({ required this.id, - required this.name, + required String name, required this.relayUrl, this.pubkey, this.nsec, @@ -54,7 +62,7 @@ class Community { this.pushLeaseInstallationId, this.starterSetupIncomplete = false, required this.addedAt, - }); + }) : _name = name; factory Community.create({ required String name, @@ -91,7 +99,7 @@ class Community { }) { return Community( id: id, - name: name ?? this.name, + name: name ?? _name, relayUrl: relayUrl ?? this.relayUrl, pubkey: pubkey == _sentinel ? this.pubkey : pubkey as String?, nsec: nsec == _sentinel ? this.nsec : nsec as String?, @@ -112,7 +120,7 @@ class Community { Map toJson() => { 'id': id, - 'name': name, + 'name': _name, 'relayUrl': relayUrl, if (pubkey != null) 'pubkey': pubkey, if (nsec != null) 'nsec': nsec, diff --git a/mobile/lib/shared/deeplink/deep_link.dart b/mobile/lib/shared/deeplink/deep_link.dart index 14c38489c6..b4a67b10f0 100644 --- a/mobile/lib/shared/deeplink/deep_link.dart +++ b/mobile/lib/shared/deeplink/deep_link.dart @@ -375,8 +375,14 @@ class NostrBindDeepLink extends BuzzDeepLink { class AddCommunityDeepLink extends BuzzDeepLink { @override final String relayUrl; - final String name; - const AddCommunityDeepLink({required this.relayUrl, required this.name}); + + final String _name; + + /// User-facing label, shortening hosted host-name fallbacks like [Community]. + String get name => HostedEnvironment.communityDisplayName(_name, relayUrl); + + const AddCommunityDeepLink({required this.relayUrl, required String name}) + : _name = name; } bool _webHandoffShape(Uri uri, String host, Set allowed) => diff --git a/mobile/test/features/channels/channels_page_test.dart b/mobile/test/features/channels/channels_page_test.dart index 542207bfc4..87f3977ea4 100644 --- a/mobile/test/features/channels/channels_page_test.dart +++ b/mobile/test/features/channels/channels_page_test.dart @@ -1669,8 +1669,9 @@ void main() { ), Community( id: 'bravo', - name: 'Bravo', - relayUrl: 'wss://bravo.example.com', + // Hosted communities without relay metadata store their host. + name: 'bravo.communities.buzz.xyz', + relayUrl: 'wss://bravo.communities.buzz.xyz', addedAt: DateTime(2025), ), ]; @@ -1717,7 +1718,11 @@ void main() { expect(find.byTooltip('Close community switcher'), findsOneWidget); expect(find.text('alpha.example.com'), findsNothing); - expect(find.text('bravo.example.com'), findsNothing); + expect(find.text('bravo.communities.buzz.xyz'), findsNothing); + expect( + find.descendant(of: options, matching: find.text('bravo')), + findsOneWidget, + ); expect(find.text('Rename'), findsNothing); expect( @@ -1790,13 +1795,13 @@ void main() { expect(activeSelection, findsNothing); expect(inactiveSelection, findsNothing); - await tester.tap(find.byTooltip('Remove Bravo')); + await tester.tap(find.byTooltip('Remove bravo')); await tester.pumpAndSettle(); expect(find.text('Remove community?'), findsOneWidget); expect( find.text( - 'Are you sure you want to remove “Bravo”? ' + 'Are you sure you want to remove “bravo”? ' 'You can pair with it again later.', ), findsOneWidget, @@ -1807,7 +1812,7 @@ void main() { expect(communityNotifier.removedIds, ['bravo']); expect(find.text('Switch Community'), findsOneWidget); - expect(find.text('Bravo'), findsNothing); + expect(find.text('bravo'), findsNothing); expect(find.text('Done'), findsOneWidget); await tester.tap(find.text('Done')); diff --git a/mobile/test/features/onboarding/hosted_community_labels_test.dart b/mobile/test/features/onboarding/hosted_community_labels_test.dart index 27144531de..d7c69a8328 100644 --- a/mobile/test/features/onboarding/hosted_community_labels_test.dart +++ b/mobile/test/features/onboarding/hosted_community_labels_test.dart @@ -49,7 +49,7 @@ void main() { find.byType(CommunityGridOption), ); expect(tile.relayUrl, relay); - expect(fixture.flow.communities.single.name, name); + expect(fixture.flow.communities.single.name, expected); if (name != expected) expect(find.text(name), findsNothing); await tester.pumpWidget(const SizedBox.shrink()); } diff --git a/mobile/test/features/onboarding/hosted_journeys_test.dart b/mobile/test/features/onboarding/hosted_journeys_test.dart index 0318db733d..914b4e862b 100644 --- a/mobile/test/features/onboarding/hosted_journeys_test.dart +++ b/mobile/test/features/onboarding/hosted_journeys_test.dart @@ -1084,7 +1084,7 @@ void main() { findsOneWidget, ); final firstSaved = (await f.storage.loadAll()).firstWhere( - (c) => c.name == 'one.communities.buzz.xyz', + (c) => c.relayUrl == 'wss://one.communities.buzz.xyz', ); Navigator.of( tester.element(find.byType(ProfileSetupPage, skipOffstage: false)), @@ -1108,16 +1108,18 @@ void main() { isTrue, ); expect( - saved.singleWhere((c) => c.name == 'one.communities.buzz.xyz').id, + saved + .singleWhere((c) => c.relayUrl == 'wss://one.communities.buzz.xyz') + .id, firstSaved.id, ); expect(await f.storage.loadActiveId(), firstSaved.id); expect( - f.probes.where((c) => c.name == 'one.communities.buzz.xyz'), + f.probes.where((c) => c.relayUrl == 'wss://one.communities.buzz.xyz'), hasLength(1), ); expect( - f.probes.where((c) => c.name == 'two.communities.buzz.xyz'), + f.probes.where((c) => c.relayUrl == 'wss://two.communities.buzz.xyz'), hasLength(2), ); expect(find.byType(OnboardingAppEntry), findsNothing); @@ -1173,11 +1175,14 @@ void main() { final communities = await f.storage.loadAll(); expect(communities, hasLength(2)); expect( - communities.map((c) => c.name), - containsAll(['one.communities.buzz.xyz', 'two.communities.buzz.xyz']), + communities.map((c) => c.relayUrl), + containsAll([ + 'wss://one.communities.buzz.xyz', + 'wss://two.communities.buzz.xyz', + ]), ); final saved = communities.singleWhere( - (c) => c.name == 'two.communities.buzz.xyz', + (c) => c.relayUrl == 'wss://two.communities.buzz.xyz', ); expect(saved.nsec, f.keys.nsec); expect(saved.pubkey, f.keys.public); diff --git a/mobile/test/features/onboarding/onboarding_entry_routes_test.dart b/mobile/test/features/onboarding/onboarding_entry_routes_test.dart index 00e4f9e993..ae8dd029d8 100644 --- a/mobile/test/features/onboarding/onboarding_entry_routes_test.dart +++ b/mobile/test/features/onboarding/onboarding_entry_routes_test.dart @@ -200,7 +200,7 @@ void main() { f.beforeAdmission = null; await tester.tap(find.text('Try again').hitTestable()); await tester.pumpAndSettle(); - expect(f.admitted, ['paired']); + expect(f.admitted, ['paired.communities.buzz.xyz']); expect(f.remembered, contains('wss://paired.communities.buzz.xyz')); expect(find.byType(OnboardingAppEntry), findsNothing); expect(find.text('Add community').hitTestable(), findsOneWidget); diff --git a/mobile/test/features/pairing/pairing_hosted_handoff_test.dart b/mobile/test/features/pairing/pairing_hosted_handoff_test.dart index 474b6d5ad7..048573da39 100644 --- a/mobile/test/features/pairing/pairing_hosted_handoff_test.dart +++ b/mobile/test/features/pairing/pairing_hosted_handoff_test.dart @@ -48,7 +48,7 @@ void main() { await container.read(pairingProvider.notifier).pair(_payload(f.keys)); await settled.future.timeout(const Duration(seconds: 2)); expect(legacyProbes, 0); - expect(f.admitted, backup ? ['paired'] : isEmpty); + expect(f.admitted, backup ? ['paired.communities.buzz.xyz'] : isEmpty); expect((await f.store.read())?.pubkey, f.keys.public); }, ); diff --git a/mobile/test/helpers/hosted_communities_fixture.dart b/mobile/test/helpers/hosted_communities_fixture.dart index fe6caf7942..5b00911ea8 100644 --- a/mobile/test/helpers/hosted_communities_fixture.dart +++ b/mobile/test/helpers/hosted_communities_fixture.dart @@ -180,9 +180,10 @@ class HostedCommunitiesFixture { if (current()) openedCommunity = link.relayUrl; }, enterCommunity: (link, subject, pubkey, current) async { - final target = link as AddCommunityDeepLink; - await beforeAdmission?.call(target.name); - if (current()) admitted.add(target.name); + // Record the relay host: names are display labels. + final host = Uri.parse((link as AddCommunityDeepLink).relayUrl).host; + await beforeAdmission?.call(host); + if (current()) admitted.add(host); }, ); }