Files
buzz/mobile/lib/features/invites/invite_join_sheet.dart
klopez4212 bbcbd7ce13 Preview Tabler icons in the mobile app (#8109)
## Summary

Preview buzz-app's Tabler icon style in the mobile app, replacing
app-drawn Lucide glyphs across channels, messages, activity, search, and
settings. A shared `BuzzIcons` mapping uses the bundled Tabler 3.46.0
outline font, matching buzz-app's version, with its MIT license and
source documented.

Native UIKit controls retain SF Symbols. The former 300/500 navigation
glyphs use Tabler's standard outline; existing selection colors and
labels remain.

### Related issue

No matching Tabler mobile migration issue or PR found. Related native
iOS controls: #8055.

### Testing

- Pre-push mobile analysis, formatting, file-size checks, and the full
mobile test suite passed.
- 194 focused widget tests passed, including channels, home, search,
settings, and message content.
- Light and dark community-settings renders inspected.
- Signed iPhone Release build succeeded; installed and launched as Buzz
Tabler on a physical iPhone.
- Full `just ci` attempted; stopped during Rust clippy compilation
because the Mac ran low on disk space. Full repository validation is not
claimed.

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-08 10:40:12 -04: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:buzz/shared/theme/buzz_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(BuzzIcons.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(BuzzIcons.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(
BuzzIcons.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(BuzzIcons.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',
),
),
],
),
),
);
}
}