Files
buzz/mobile/lib/features/settings/settings_page/connection_section.dart
klopez4212 a567dfc2df Unify mobile channel details (#6113)
## 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

![Channel header with channel name and member
count](https://raw.githubusercontent.com/block/buzz/c61c64a8db0f69fc71e0ac344d64384a85ad02b5/pr-6113--01-channel-header.png)

### Combined channel details

![Combined channel details with actions and member
preview](https://raw.githubusercontent.com/block/buzz/c61c64a8db0f69fc71e0ac344d64384a85ad02b5/pr-6113--02-channel-details-members.png)

### Manage channel

![Manage channel editing
controls](https://raw.githubusercontent.com/block/buzz/c61c64a8db0f69fc71e0ac344d64384a85ad02b5/pr-6113--03-manage-channel.png)

---------

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>
2026-08-19 17:48:09 +01:00

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'),
),
],
),
);
}