mirror of
https://github.com/block/buzz.git
synced 2026-11-04 16:31:38 +00:00
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>
188 lines
6.4 KiB
Dart
188 lines
6.4 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
import '../hosted/hosted_environment.dart';
|
|
import '../push/push_subscription.dart';
|
|
|
|
const _uuid = Uuid();
|
|
const _sentinel = Object();
|
|
final _pushLeaseInstallationIdPattern = RegExp(r'^[0-9a-f]{32}$');
|
|
|
|
String _newPushLeaseInstallationId() {
|
|
final random = Random.secure();
|
|
return List.generate(
|
|
16,
|
|
(_) => random.nextInt(256).toRadixString(16).padLeft(2, '0'),
|
|
).join();
|
|
}
|
|
|
|
enum SensitiveActionPolicy { enabled, disabledByUser }
|
|
|
|
class Community {
|
|
static const _defaultPushNotificationsEnabled = true;
|
|
|
|
static const _pushNotificationsEnabledKey = 'pushNotificationsEnabledV2';
|
|
|
|
final String id;
|
|
|
|
/// 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;
|
|
final SensitiveActionPolicy sensitiveActionPolicy;
|
|
final bool pushNotificationsEnabled;
|
|
final BuzzPushLeaseSubscriptionState pushSubscriptionState;
|
|
|
|
/// Stable random address component for this community's relay push lease.
|
|
///
|
|
/// Legacy records omit this value and continue using the endpoint grant's
|
|
/// installation id so their already-published lease remains addressable.
|
|
final String? pushLeaseInstallationId;
|
|
|
|
/// Whether invite-created starter channels still need to be recovered.
|
|
final bool starterSetupIncomplete;
|
|
final DateTime addedAt;
|
|
|
|
const Community({
|
|
required this.id,
|
|
required String name,
|
|
required this.relayUrl,
|
|
this.pubkey,
|
|
this.nsec,
|
|
this.sensitiveActionPolicy = SensitiveActionPolicy.disabledByUser,
|
|
this.pushNotificationsEnabled = _defaultPushNotificationsEnabled,
|
|
this.pushSubscriptionState = const BuzzPushLeaseSubscriptionState.desired(),
|
|
this.pushLeaseInstallationId,
|
|
this.starterSetupIncomplete = false,
|
|
required this.addedAt,
|
|
}) : _name = name;
|
|
|
|
factory Community.create({
|
|
required String name,
|
|
required String relayUrl,
|
|
String? pubkey,
|
|
String? nsec,
|
|
SensitiveActionPolicy sensitiveActionPolicy =
|
|
SensitiveActionPolicy.disabledByUser,
|
|
bool starterSetupIncomplete = false,
|
|
}) {
|
|
return Community(
|
|
id: _uuid.v4(),
|
|
name: name,
|
|
relayUrl: relayUrl,
|
|
pubkey: pubkey,
|
|
nsec: nsec,
|
|
sensitiveActionPolicy: sensitiveActionPolicy,
|
|
pushLeaseInstallationId: _newPushLeaseInstallationId(),
|
|
starterSetupIncomplete: starterSetupIncomplete,
|
|
addedAt: DateTime.now(),
|
|
);
|
|
}
|
|
|
|
Community copyWith({
|
|
String? name,
|
|
String? relayUrl,
|
|
Object? pubkey = _sentinel,
|
|
Object? nsec = _sentinel,
|
|
SensitiveActionPolicy? sensitiveActionPolicy,
|
|
bool? pushNotificationsEnabled,
|
|
BuzzPushLeaseSubscriptionState? pushSubscriptionState,
|
|
Object? pushLeaseInstallationId = _sentinel,
|
|
bool? starterSetupIncomplete,
|
|
}) {
|
|
return Community(
|
|
id: id,
|
|
name: name ?? _name,
|
|
relayUrl: relayUrl ?? this.relayUrl,
|
|
pubkey: pubkey == _sentinel ? this.pubkey : pubkey as String?,
|
|
nsec: nsec == _sentinel ? this.nsec : nsec as String?,
|
|
sensitiveActionPolicy:
|
|
sensitiveActionPolicy ?? this.sensitiveActionPolicy,
|
|
pushNotificationsEnabled:
|
|
pushNotificationsEnabled ?? this.pushNotificationsEnabled,
|
|
pushSubscriptionState:
|
|
pushSubscriptionState ?? this.pushSubscriptionState,
|
|
pushLeaseInstallationId: pushLeaseInstallationId == _sentinel
|
|
? this.pushLeaseInstallationId
|
|
: pushLeaseInstallationId as String?,
|
|
starterSetupIncomplete:
|
|
starterSetupIncomplete ?? this.starterSetupIncomplete,
|
|
addedAt: addedAt,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'id': id,
|
|
'name': _name,
|
|
'relayUrl': relayUrl,
|
|
if (pubkey != null) 'pubkey': pubkey,
|
|
if (nsec != null) 'nsec': nsec,
|
|
'sensitiveActionPolicy': sensitiveActionPolicy.name,
|
|
_pushNotificationsEnabledKey: pushNotificationsEnabled,
|
|
'pushSubscriptionState': pushSubscriptionState.toJson(),
|
|
if (pushLeaseInstallationId != null)
|
|
'pushLeaseInstallationId': pushLeaseInstallationId,
|
|
'starterSetupIncomplete': starterSetupIncomplete,
|
|
'addedAt': addedAt.toIso8601String(),
|
|
};
|
|
|
|
factory Community.fromJson(Map<String, dynamic> json) {
|
|
final pushLeaseInstallationId = json['pushLeaseInstallationId'] as String?;
|
|
if (pushLeaseInstallationId != null &&
|
|
!_pushLeaseInstallationIdPattern.hasMatch(pushLeaseInstallationId)) {
|
|
throw const FormatException(
|
|
'Push lease installation id must be 16 random bytes encoded as lowercase hex',
|
|
);
|
|
}
|
|
final pushNotificationsEnabled =
|
|
json[_pushNotificationsEnabledKey] as bool? ??
|
|
_defaultPushNotificationsEnabled;
|
|
var pushSubscriptionState = json['pushSubscriptionState'] == null
|
|
? const BuzzPushLeaseSubscriptionState.desired()
|
|
: BuzzPushLeaseSubscriptionState.fromJson(
|
|
Map<String, dynamic>.from(json['pushSubscriptionState'] as Map),
|
|
);
|
|
if (!pushNotificationsEnabled &&
|
|
pushSubscriptionState.pendingTombstoneGeneration == null) {
|
|
pushSubscriptionState = pushSubscriptionState
|
|
.withPendingTombstoneAtCursor();
|
|
}
|
|
return Community(
|
|
id: json['id'] as String,
|
|
name: json['name'] as String,
|
|
relayUrl: json['relayUrl'] as String,
|
|
pubkey: json['pubkey'] as String?,
|
|
nsec: json['nsec'] as String?,
|
|
sensitiveActionPolicy: SensitiveActionPolicy.values.firstWhere(
|
|
(value) => value.name == json['sensitiveActionPolicy'],
|
|
orElse: () => SensitiveActionPolicy.disabledByUser,
|
|
),
|
|
pushNotificationsEnabled: pushNotificationsEnabled,
|
|
pushSubscriptionState: pushSubscriptionState,
|
|
pushLeaseInstallationId: pushLeaseInstallationId,
|
|
starterSetupIncomplete: json['starterSetupIncomplete'] as bool? ?? false,
|
|
addedAt: DateTime.parse(json['addedAt'] as String),
|
|
);
|
|
}
|
|
|
|
/// Derive a human-friendly community name from a relay URL.
|
|
static String nameFromUrl(String url) {
|
|
try {
|
|
final host = Uri.parse(url).host;
|
|
if (host.contains('localhost') || host == '127.0.0.1') return 'Local Dev';
|
|
final parts = host.split('.');
|
|
if (parts.length > 2) return parts.first;
|
|
return host;
|
|
} catch (_) {
|
|
return 'Community';
|
|
}
|
|
}
|
|
}
|