Files
buzz/mobile/lib/features/invites/invite_join_sheet.dart
klopez4212 ed11c8d8bf Refresh mobile utility surfaces and theme picker (#6944)
## Summary

- invert mobile utility surfaces so pages and sheets use the softer page
background with raised containers
- rebuild the community theme picker around swipeable Home/Chat
previews, accent and appearance controls, and native iOS glass
interactions
- align shared sheet/profile spacing and radii, and move Theme into the
Community settings card

### Related issue

None found.

### Testing

- `just mobile-check`
- `just mobile-test` (1,871 tests)
- signed iOS Release build installed on a physical iPhone

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
2026-08-28 18:56:42 +01:00

247 lines
8.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../shared/theme/theme.dart';
import '../../shared/widgets/buzz_loading_indicator.dart';
import '../../shared/widgets/modal_presentation.dart';
import '../pairing/pairing_page.dart';
import 'invite_join_provider.dart';
Future<bool?> showInviteJoinSheet(BuildContext context) {
return showBuzzModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
// The route remains a normal modal while idle, but its contents install a
// PopScope once a membership claim or starter setup begins. Keep drag and
// the shared close affordance out of this sheet so they cannot bypass that
// in-flight guard.
enableDrag: false,
showCloseButton: false,
builder: (_) => const _InviteJoinSheetRoute(),
);
}
class _InviteJoinSheetRoute extends ConsumerWidget {
const _InviteJoinSheetRoute();
@override
Widget build(BuildContext context, WidgetRef ref) {
final isClaiming =
ref.watch(inviteJoinProvider).status == InviteJoinStatus.claiming;
return PopScope(canPop: !isClaiming, child: const InviteJoinSheet());
}
}
class InviteJoinSheet extends ConsumerWidget {
const InviteJoinSheet({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(inviteJoinProvider);
final isClaiming = state.status == InviteJoinStatus.claiming;
final isStarterSetupRecovery = state.isStarterSetupRecovery;
final host = state.host ?? 'unknown host';
final derivedName = state.communityName;
final primaryLabel = switch ((
isClaiming,
isStarterSetupRecovery,
state.status,
)) {
(true, true, _) => 'Finishing setup…',
(true, false, _) => 'Joining…',
(false, true, InviteJoinStatus.error) => 'Retry setup',
(false, true, _) => 'Finish setting up',
_ => 'Join',
};
if (state.status == InviteJoinStatus.success) {
return _InviteJoinSuccess(
host: host,
communityName: derivedName,
hasFocusChannel: state.focusChannelId != null,
);
}
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(Grid.sm, 0, Grid.sm, Grid.sm),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(LucideIcons.userPlus, size: 40, color: context.colors.primary),
const SizedBox(height: Grid.sm),
Text(
isStarterSetupRecovery
? 'Finish setting up'
: 'Join this Buzz community?',
style: context.textTheme.titleLarge,
),
const SizedBox(height: Grid.xxs),
Text(
'Check the relay host before you join:',
style: context.textTheme.bodyMedium?.copyWith(
color: context.colors.onSurfaceVariant,
),
),
const SizedBox(height: Grid.xs),
Container(
padding: const EdgeInsets.all(Grid.twelve),
decoration: BoxDecoration(
color: context.colors.surfaceContainerHighest.withValues(
alpha: 0.7,
),
borderRadius: BorderRadius.circular(Radii.container),
border: Border.all(color: context.colors.outlineVariant),
),
child: Text(
host,
style: context.textTheme.titleMedium?.copyWith(
fontFamily: 'GeistMono',
fontWeight: FontWeight.w700,
),
),
),
if (derivedName != null && derivedName != host) ...[
const SizedBox(height: Grid.xxs),
Text(
'Display name: $derivedName',
style: context.textTheme.bodySmall?.copyWith(
color: context.colors.onSurfaceVariant,
),
),
],
const SizedBox(height: Grid.sm),
if (isStarterSetupRecovery) ...[
Text(
'Your membership is ready, but starter channels still need to be set up. You can safely retry without claiming the invite again.',
style: context.textTheme.bodyMedium?.copyWith(
color: context.colors.onSurfaceVariant,
),
),
const SizedBox(height: Grid.sm),
],
Text(
'This phone is the only copy of this identity. If you lose it before pairing or backing up, you’ll lose access as this member.',
style: context.textTheme.bodyMedium?.copyWith(
color: context.colors.onSurfaceVariant,
),
),
if (state.status == InviteJoinStatus.error &&
state.errorMessage != null) ...[
const SizedBox(height: Grid.sm),
Text(
state.errorMessage!,
style: context.textTheme.bodySmall?.copyWith(
color: context.colors.error,
),
),
],
const SizedBox(height: Grid.lg),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: isClaiming
? null
: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
),
const SizedBox(width: Grid.sm),
Expanded(
child: FilledButton.icon(
onPressed: isClaiming || state.requiresFreshInvite
? null
: () async => ref
.read(inviteJoinProvider.notifier)
.confirmJoin(),
icon: isClaiming
? SizedBox(
width: 16,
height: 16,
child: BuzzLoadingIndicator(
size: 16,
semanticLabel: isStarterSetupRecovery
? 'Finishing setup'
: 'Joining community',
),
)
: const Icon(LucideIcons.check),
label: Text(primaryLabel),
),
),
],
),
],
),
),
);
}
}
class _InviteJoinSuccess extends StatelessWidget {
final String host;
final String? communityName;
final bool hasFocusChannel;
const _InviteJoinSuccess({
required this.host,
required this.hasFocusChannel,
this.communityName,
});
@override
Widget build(BuildContext context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(Grid.sm, 0, Grid.sm, Grid.sm),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(
LucideIcons.circleCheck,
size: 40,
color: context.colors.primary,
),
const SizedBox(height: Grid.sm),
Text(
'You joined ${communityName ?? host}',
style: context.textTheme.titleLarge,
),
const SizedBox(height: Grid.xs),
Text(
'This phone is the only copy of this identity. If you lose it before pairing or backing up, you’ll lose access as this member.',
style: context.textTheme.bodyMedium?.copyWith(
color: context.colors.onSurfaceVariant,
),
),
const SizedBox(height: Grid.lg),
FilledButton.icon(
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const PairingPage(addingCommunity: true),
),
);
},
icon: const Icon(LucideIcons.scanLine),
label: const Text('Back it up now'),
),
const SizedBox(height: Grid.xs),
TextButton(
onPressed: () => Navigator.of(context).pop(hasFocusChannel),
child: Text(
hasFocusChannel ? 'Continue to #welcome-everyone' : 'Not now',
),
),
],
),
),
);
}
}