Files
buzz/mobile/lib/shared/community/community.dart
Tom Brow 6eff84d127 fix(mobile): join starter channels after accepting invite (#5915)
### What changed?

After a new mobile invite claim succeeds, Buzz now best-effort ensures
membership in the same public starter channels as desktop:

- `#general`
- `#welcome-everyone`

Missing starters use desktop's deterministic per-relay IDs and exact
public channel configuration, so concurrent mobile and desktop setup
converges safely. Setup failures do not invalidate or retry an already
successful invite claim, and failure for one starter does not block the
other.

The success sheet offers **Continue to #welcome-everyone** when that
channel is available. Mobile does not create the private `Welcome`
channel because it cannot provision the desktop Welcome agents that make
that channel useful.

This PR is stacked on #6145 because it deliberately reuses that PR's
open-channel directory and join behavior. Once #6145 merges, this PR can
be retargeted to `main` without changing its BUZZ-12 diff.

Fixes
[BUZZ-12](https://linear.app/squareup/issue/BUZZ-12/bug-community-appears-empty-after-using-invite-link-on-mobile).

### How is it tested?

- Desktop/mobile deterministic starter-ID parity coverage.
- Existing-channel join and missing-channel creation coverage.
- Duplicate-create convergence and per-channel failure isolation
coverage.
- Invite success remains successful when starter setup fails.
- Widget coverage for continuing directly into `#welcome-everyone`.
- Focused invite/deep-link tests: 23 passed.
- `just mobile-check`: passed.
- `just mobile-test`: 1,483 passed.
- Pre-push repository checks: passed.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Codex <noreply@openai.com>
2026-08-24 12:59:21 -07:00

112 lines
3.2 KiB
Dart

import 'package:uuid/uuid.dart';
const _uuid = Uuid();
const _sentinel = Object();
enum SensitiveActionPolicy { enabled, disabledByUser }
class Community {
final String id;
final String name;
final String relayUrl;
final String? pubkey;
final String? nsec;
final SensitiveActionPolicy sensitiveActionPolicy;
/// Whether invite-created starter channels still need to be recovered.
final bool starterSetupIncomplete;
final DateTime addedAt;
const Community({
required this.id,
required this.name,
required this.relayUrl,
this.pubkey,
this.nsec,
this.sensitiveActionPolicy = SensitiveActionPolicy.disabledByUser,
this.starterSetupIncomplete = false,
required this.addedAt,
});
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,
starterSetupIncomplete: starterSetupIncomplete,
addedAt: DateTime.now(),
);
}
Community copyWith({
String? name,
String? relayUrl,
Object? pubkey = _sentinel,
Object? nsec = _sentinel,
SensitiveActionPolicy? sensitiveActionPolicy,
bool? starterSetupIncomplete,
}) {
return Community(
id: id,
name: name ?? this.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,
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,
'starterSetupIncomplete': starterSetupIncomplete,
'addedAt': addedAt.toIso8601String(),
};
factory Community.fromJson(Map<String, dynamic> json) => 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,
),
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';
}
}
}