mirror of
https://github.com/block/buzz.git
synced 2026-11-02 07:22:55 +00:00
fix(mobile): show hosted community names consistently (#8872)
When a hosted relay has no metadata name, the hosted add paths save its host (`remote-agent-pilot.communities.buzz.xyz`) as the community name. Onboarding shortens that fallback to `remote-agent-pilot`. After joining, though, the community switcher, the remove dialog, the theme preview and the identity export showed the full host, so the same community appeared under two names. This change shortens the name in the model instead of at each call site. `Community` and `AddCommunityDeepLink` keep the saved value private, and their public `name` getter returns the display form, using the existing `HostedEnvironment.communityDisplayName` rule. Existing and future readers get the short name without opting in. Serialization still writes the saved value, so stored data is unchanged and no migration is needed. Custom names and non-hosted hosts pass through unchanged, as before. Community identity is unaffected because it uses `id` and `relayUrl`, never the name. Beyond the captures below, the same getter shortens the name in the iOS push notification subtitle, onboarding's "Adding …" progress text, the "You joined …" invite sheet, and the VoiceOver labels for removing and loading a community. ### Before and after | | Before (`main`) | After | |---|---|---| | Community switcher |  |  | | Remove dialog |  |  | | Theme preview |  |  | | Identity export |  |  | ### Before merge - [x] Kenny Lopez reviews the visible changes ([approved in Buzz #buzz-mobile](buzz://message?channel=9f18f3c6-b5e5-453d-b36d-65487913be0b&id=c8b01eaa3760e44c3b6583f2e02bec576d78679adfb364e5fe718cc0926bdc2d)). ### Validation - Captures come from iPhone 17 simulators (iOS 27) running `main` and this branch. Both builds used the same scratch-only seed with two communities whose saved names are their hosted hosts. The seed is not part of this PR. The relays were unreachable from the simulator, so the screens show loading placeholders. - Changed tests: the community switcher journey now uses a hosted host-name fallback for one community. It asserts the grid label, the remove tooltip and the dialog text show the short name, and it fails when the getter returns the saved value. The onboarding label table now asserts the model's `name` itself is shortened. The hosted-flow fixture records relay hosts instead of names, and the auth-domain tests look up communities by relay URL. - The full mobile test suite and `flutter analyze` pass. The CI step that runs with configured authentication domains also passes locally (113 tests). - Not captured: the push notification subtitle, onboarding's "Adding …" text and the invite sheet. Reaching them needs hosted sign-in or push, which the simulator doesn't support. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Tom Brow <tomb@squareup.com> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'name': _name,
|
||||
'relayUrl': relayUrl,
|
||||
if (pubkey != null) 'pubkey': pubkey,
|
||||
if (nsec != null) 'nsec': nsec,
|
||||
|
||||
@@ -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<String> allowed) =>
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user