Files
buzz/mobile/lib/features/profile/profile_text_edit_sheet.dart
klopez4212 27b9c827fc Polish mobile message layout and navigation controls (#8427)
Align outgoing messages and galleries to the right, reclaim the outgoing
avatar gutter, and match desktop calendar dividers with balanced
spacing. Standardize navigation and sheet icons at 24 points, restore
native Edit/Done styling, and align section menus with trailing channel
indicators.

Validation: clean analysis; 3,513 mobile tests and push-configuration
checks passed; iPhone Release build installed and tested by the owner.
Includes regression coverage for message placement, date spacing,
gallery direction, and section alignment.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: fin <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-18 15:17:25 -04:00

166 lines
5.7 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:buzz/shared/theme/buzz_icons.dart';
import '../../shared/theme/theme.dart';
import '../../shared/widgets/buzz_sheet_header.dart';
import '../../shared/widgets/ios_glass_navigation_button.dart';
import 'profile_provider.dart';
/// Flutter fallback sheet for editing one profile text field.
class ProfileTextEditSheet extends HookWidget {
/// Creates a profile text editor.
const ProfileTextEditSheet({
super.key,
required this.title,
required this.initialValue,
required this.hintText,
required this.multiline,
required this.onSave,
});
/// The heading displayed in the sheet header.
final String title;
/// The value loaded into the text field when the sheet opens.
final String initialValue;
/// Placeholder text displayed while the text field is empty.
final String hintText;
/// Whether the text field accepts and displays multiple lines.
final bool multiline;
/// Persists the submitted value before the sheet closes.
final Future<void> Function(String value) onSave;
@override
Widget build(BuildContext context) {
final controller = useTextEditingController(text: initialValue);
useListenable(controller);
final isSaving = useState(false);
final error = useState<String?>(null);
final hasChanges = controller.text.trim() != initialValue.trim();
Future<void> closeAfterSave() async {
isSaving.value = false;
await WidgetsBinding.instance.endOfFrame;
if (context.mounted) Navigator.of(context).pop();
}
Future<void> save() async {
if (!hasChanges || isSaving.value) return;
isSaving.value = true;
error.value = null;
try {
await onSave(controller.text);
if (context.mounted) await closeAfterSave();
} on ProfileCommunityChangedException {
if (context.mounted) await closeAfterSave();
} catch (_) {
if (context.mounted) {
error.value = "We couldn't save this change. Try again.";
}
} finally {
if (context.mounted) isSaving.value = false;
}
}
final closeButton = Theme.of(context).platform == TargetPlatform.iOS
? IosGlassNavigationButton(
key: const ValueKey('profile-field-close'),
icon: IosGlassNavigationIcon.close,
semanticLabel: 'Close sheet',
onPressed: isSaving.value
? null
: () => Navigator.of(context).pop(),
width: 44,
height: 44,
foregroundColor: context.colors.primary,
)
: SizedBox.square(
dimension: 44,
child: IconButton(
key: const ValueKey('profile-field-close'),
tooltip: 'Close sheet',
onPressed: isSaving.value
? null
: () => Navigator.of(context).pop(),
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
backgroundColor: context.colors.surfaceContainerHighest,
foregroundColor: context.colors.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(Radii.dialog),
),
),
icon: const Icon(BuzzIcons.x, size: 24),
),
);
return PopScope<void>(
canPop: !isSaving.value,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
BuzzSheetHeader(title: title, trailing: closeButton),
Flexible(
child: SafeArea(
top: false,
child: SingleChildScrollView(
key: const ValueKey('profile-field-scroll-view'),
padding: EdgeInsets.fromLTRB(
Grid.gutter,
Grid.xxs,
Grid.gutter,
MediaQuery.viewInsetsOf(context).bottom + Grid.xs,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
key: const ValueKey('profile-field-input'),
controller: controller,
autofocus: true,
enabled: !isSaving.value,
minLines: multiline ? 4 : 1,
maxLines: multiline ? 6 : 1,
textCapitalization: TextCapitalization.sentences,
textInputAction: multiline
? TextInputAction.newline
: TextInputAction.done,
onSubmitted: multiline ? null : (_) => unawaited(save()),
decoration: InputDecoration(hintText: hintText),
),
if (error.value != null) ...[
const SizedBox(height: Grid.xxs),
Semantics(
liveRegion: true,
child: Text(
error.value!,
style: context.textTheme.bodySmall?.copyWith(
color: context.colors.error,
),
),
),
],
const SizedBox(height: Grid.xs),
FilledButton(
key: const ValueKey('profile-field-save'),
onPressed: hasChanges && !isSaving.value ? save : null,
child: Text(isSaving.value ? 'Saving…' : 'Save'),
),
],
),
),
),
),
],
),
);
}
}