mirror of
https://github.com/block/buzz.git
synced 2026-11-01 15:01:12 +00:00
## Summary - combine channel identity, actions, member previews, and lifecycle controls in one navigable mobile details page - add compact member management and desktop-aligned channel editing - smooth the profile entrance transition and standardize settings-group spacing - align Huddle lifecycle rows with regular message author and body spacing ## Testing - `just mobile-check` - `just mobile-test` — 1,477 tests - Pixel 10 debug build, install, and interaction review ## Snapshots ### Channel header  ### Combined channel details  ### Manage channel  --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz> Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz>
180 lines
5.4 KiB
Dart
180 lines
5.4 KiB
Dart
part of '../settings_page.dart';
|
|
|
|
class _ConnectionSection extends ConsumerWidget {
|
|
const _ConnectionSection({required this.identityRecoveryPageBuilder});
|
|
|
|
final WidgetBuilder identityRecoveryPageBuilder;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final config = ref.watch(relayConfigProvider);
|
|
final authState = ref.watch(authProvider).value;
|
|
final nsec = config.nsec;
|
|
final community = authState?.community;
|
|
|
|
return AppListCard(
|
|
label: 'Connection',
|
|
verticalPadding: Grid.twelve,
|
|
children: [
|
|
AppListRow(
|
|
icon: LucideIcons.server,
|
|
title: 'Connected to',
|
|
subtitle: config.baseUrl,
|
|
),
|
|
if (nsec != null && nsec.isNotEmpty && community != null) ...[
|
|
_IdentityRow(nsec: nsec),
|
|
AppListRow(
|
|
icon: LucideIcons.scanQrCode,
|
|
title: 'Send identity to desktop',
|
|
subtitle: 'Scan a recovery code shown by Buzz Desktop',
|
|
trailing: const _RowChevron(),
|
|
onTap: () async {
|
|
final pairing = ref.read(pairingProvider.notifier);
|
|
final authorized = await pairing.authorizeIdentityExport(
|
|
community: community,
|
|
);
|
|
if (!authorized) {
|
|
if (!context.mounted) return;
|
|
final message = ref.read(pairingProvider).errorMessage;
|
|
if (message != null) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(message)));
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (!context.mounted) return;
|
|
final resumed = await _waitForResumedFrame();
|
|
if (!resumed) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'Buzz did not return to the foreground. Try again.',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
if (!context.mounted) return;
|
|
await Navigator.of(context).push(
|
|
MaterialPageRoute<void>(builder: identityRecoveryPageBuilder),
|
|
);
|
|
} finally {
|
|
pairing.reset();
|
|
}
|
|
},
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
const _resumeWaitTimeout = Duration(seconds: 5);
|
|
|
|
Future<bool> _waitForResumedFrame() async {
|
|
final binding = WidgetsBinding.instance;
|
|
if (binding.lifecycleState != AppLifecycleState.resumed) {
|
|
final resumed = Completer<void>();
|
|
final listener = AppLifecycleListener(
|
|
onResume: () {
|
|
if (!resumed.isCompleted) resumed.complete();
|
|
},
|
|
);
|
|
try {
|
|
await resumed.future.timeout(_resumeWaitTimeout);
|
|
} on TimeoutException {
|
|
return false;
|
|
} finally {
|
|
listener.dispose();
|
|
}
|
|
}
|
|
await binding.endOfFrame;
|
|
return true;
|
|
}
|
|
|
|
/// Destructive, so it gets a container of its own rather than sitting at the
|
|
/// bottom of the connection group.
|
|
class _RemoveCommunitySection extends ConsumerWidget {
|
|
const _RemoveCommunitySection();
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return AppListCard(
|
|
verticalPadding: Grid.twelve,
|
|
children: [
|
|
AppListRow(
|
|
icon: LucideIcons.logOut,
|
|
title: 'Remove community',
|
|
titleColor: context.colors.error,
|
|
onTap: () => _confirmRemoveCommunity(context, ref),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _IdentityRow extends StatelessWidget {
|
|
const _IdentityRow({required this.nsec});
|
|
|
|
final String nsec;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final privHex = nostr.Nip19.decode(payload: nsec).data;
|
|
final pubkey = privHex.isNotEmpty ? nostr.Keys(privHex).public : 'unknown';
|
|
|
|
return AppListRow(
|
|
icon: LucideIcons.key,
|
|
title: 'Identity (pubkey)',
|
|
subtitle: pubkey,
|
|
subtitleStyle: context.textTheme.bodySmall?.copyWith(
|
|
color: context.colors.onSurfaceVariant,
|
|
fontFamily: 'GeistMono',
|
|
fontSize: 11,
|
|
),
|
|
subtitleMaxLines: 2,
|
|
trailing: IconButton(
|
|
icon: const Icon(LucideIcons.copy, size: 16),
|
|
onPressed: () async {
|
|
await copyToClipboard(context, pubkey, message: 'Pubkey copied');
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
void _confirmRemoveCommunity(BuildContext context, WidgetRef ref) {
|
|
showBuzzDialog<void>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('Remove Community'),
|
|
content: const Text(
|
|
'This will disconnect this community. You will need '
|
|
'to scan a new pairing code to reconnect.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(),
|
|
child: const Text('Cancel'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () {
|
|
Navigator.of(ctx).pop(); // close dialog
|
|
// Pop all pushed routes back to root so MaterialApp.home rebuilds
|
|
// to PairingPage when auth state changes.
|
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
|
ref.read(authProvider.notifier).signOut();
|
|
},
|
|
style: FilledButton.styleFrom(backgroundColor: ctx.colors.error),
|
|
child: const Text('Remove'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|