mirror of
https://github.com/block/buzz.git
synced 2026-11-01 23:11:58 +00:00
## Summary Unify mobile conversations in Messages with All / Solo / Team filters, a collapsing header, and separate Search, New, and default-agent shortcuts. Move workspace actions into the community menu and keep collapsed sections intact across filters. Align Inbox and list typography, spacing, and muted controls. Preserve native glass layering through navigation and give every filtered list enough clearance above the footer. The default-agent shortcut reads the shared encrypted preference and opens an existing chat immediately. When unset, it opens Agents; a star action selects the default through a production signed, self-encrypted preference writer. Save failures remain visible and retryable. Android now uses rounded navigation controls and filters, optically balanced search icons, and expanding New/community menus with leading Tabler icons matching iOS. Preset avatars retain transparent WebM animation through an Android WebView renderer, with lifecycle pausing and poster fallback. ### Related issue None found in searches for mobile navigation issues and open PRs. ### Testing - Android follow-up: ARM64 debug build, Dart analysis, and emulator checks passed for menu placement, selection, icon alignment, search, and avatar playback/pause/resume. The avatar implementation passed 3,840 Flutter tests and native Android tests before subsequent styling iterations. Automated tests for the later menu/icon iterations were skipped at the author’s request; those earlier results do not cover the final styling changes. - Mobile formatting/analysis and full test suite (3,838 passed, 23 skipped, plus 4 push-configuration tests); regression journey covers filtering, collapse retention, and scrolling the last Solo row above the footer. - iPhone 18 Pro simulator (Honeycomb): filters, menus, Inbox, Invite/Back, Switch Community, and default-agent chat/Back exercised. iOS release build and Pairing Preview install/launch passed during iteration, preserving the saved profile. - P1 follow-up: production writer-to-reader widget journey covers selection, publish failure/retry, and the unset footer opening Agents. Simulator Agents open/close passes; its saved community has no selectable agents, so live selection was not verified there. - Independent agent review completed; its Solo footer-clearance finding is fixed. The final clearance adjustment is covered by widget tests; physical-device acceptance of that adjustment and hosted CI remain pending. Final Messages layout:  Switcher layering during iteration (before → after): | Before: filter edge showing through | After: Home hidden behind switcher | | --- | --- | |  |  | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
182 lines
5.9 KiB
Dart
182 lines
5.9 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:buzz/shared/theme/buzz_icons.dart';
|
|
|
|
import '../theme/theme.dart';
|
|
|
|
/// Height of an idle [BuzzSearchField].
|
|
const double buzzSearchIdleFieldHeight = 45;
|
|
|
|
/// Font size used for idle [BuzzSearchField] text.
|
|
const double buzzSearchIdleTextSize = 15;
|
|
const double _searchIdleIconSize = 26;
|
|
|
|
/// Base size for the Android search controls; search needs optical scaling.
|
|
const double buzzSearchControlIconSize = 24;
|
|
const double _searchCompactIconSize = 18;
|
|
const double _searchIdleIconInset = Grid.xxs;
|
|
const double _searchCompactTextInset =
|
|
_searchIdleIconInset + _searchCompactIconSize + Grid.xxs;
|
|
|
|
/// Buzz's global-search text field treatment, shared by search-like inputs.
|
|
class BuzzSearchField extends StatelessWidget {
|
|
/// Creates a search field with Buzz's shared styling.
|
|
const BuzzSearchField({
|
|
required this.controller,
|
|
required this.focusNode,
|
|
required this.hintText,
|
|
required this.iconColor,
|
|
required this.inputColor,
|
|
required this.placeholderColor,
|
|
required this.surfaceColor,
|
|
required this.isEditing,
|
|
required this.reduceMotion,
|
|
required this.motionDuration,
|
|
required this.onTap,
|
|
required this.onChanged,
|
|
required this.onSubmitted,
|
|
this.fieldKey = const Key('search-field'),
|
|
this.autocorrect = true,
|
|
this.enableSuggestions = true,
|
|
this.enabled = true,
|
|
this.textInputAction = TextInputAction.search,
|
|
super.key,
|
|
});
|
|
|
|
/// Controller that owns the field's editable text.
|
|
final TextEditingController controller;
|
|
|
|
/// Node that controls the field's focus.
|
|
final FocusNode focusNode;
|
|
|
|
/// Placeholder shown while the field is idle and empty.
|
|
final String hintText;
|
|
|
|
/// Color applied to the search icon.
|
|
final Color iconColor;
|
|
|
|
/// Color applied to entered text.
|
|
final Color inputColor;
|
|
|
|
/// Color applied to [hintText].
|
|
final Color placeholderColor;
|
|
|
|
/// Background color of the field.
|
|
final Color surfaceColor;
|
|
|
|
/// Whether the field is in its compact editing state.
|
|
final bool isEditing;
|
|
|
|
/// Whether to suppress field animations for reduced-motion users.
|
|
final bool reduceMotion;
|
|
|
|
/// Duration used by the field's editing-state animation.
|
|
final Duration motionDuration;
|
|
|
|
/// Called when the field is tapped.
|
|
final VoidCallback onTap;
|
|
|
|
/// Called whenever the field's text changes.
|
|
final ValueChanged<String> onChanged;
|
|
|
|
/// Called when the user submits the field.
|
|
final ValueChanged<String> onSubmitted;
|
|
|
|
/// Key assigned to the underlying text field.
|
|
final Key fieldKey;
|
|
|
|
/// Whether the text field should autocorrect user input.
|
|
final bool autocorrect;
|
|
|
|
/// Whether the platform should offer text suggestions.
|
|
final bool enableSuggestions;
|
|
|
|
/// Whether the text field accepts user input.
|
|
final bool enabled;
|
|
|
|
/// Action displayed on the keyboard's submit key.
|
|
final TextInputAction textInputAction;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final idleIconSize = defaultTargetPlatform == TargetPlatform.android
|
|
? buzzSearchControlIconSize * 0.75
|
|
: _searchIdleIconSize;
|
|
final idleTextInset = _searchIdleIconInset + idleIconSize + Grid.xxs;
|
|
return DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
color: surfaceColor,
|
|
borderRadius: BorderRadius.circular(
|
|
defaultTargetPlatform == TargetPlatform.android
|
|
? Radii.full
|
|
: Radii.lg,
|
|
),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
child: TextField(
|
|
key: fieldKey,
|
|
controller: controller,
|
|
focusNode: focusNode,
|
|
autocorrect: autocorrect,
|
|
enableSuggestions: enableSuggestions,
|
|
enabled: enabled,
|
|
decoration: InputDecoration(
|
|
hintText: isEditing ? null : hintText,
|
|
hintStyle: searchInputTextStyle.copyWith(
|
|
color: placeholderColor,
|
|
fontSize: buzzSearchIdleTextSize,
|
|
height: 20 / buzzSearchIdleTextSize,
|
|
),
|
|
border: InputBorder.none,
|
|
enabledBorder: InputBorder.none,
|
|
focusedBorder: InputBorder.none,
|
|
isDense: true,
|
|
contentPadding: EdgeInsets.only(
|
|
left: isEditing ? _searchCompactTextInset : idleTextInset,
|
|
right: Grid.xxs,
|
|
top: isEditing ? Grid.xxs : 0,
|
|
bottom: isEditing ? Grid.xxs : 0,
|
|
),
|
|
),
|
|
style: searchInputTextStyle.copyWith(color: inputColor),
|
|
textAlignVertical: TextAlignVertical.center,
|
|
textAlign: TextAlign.start,
|
|
textInputAction: textInputAction,
|
|
onTap: onTap,
|
|
onChanged: onChanged,
|
|
onSubmitted: onSubmitted,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
IgnorePointer(
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(left: _searchIdleIconInset),
|
|
child: AnimatedScale(
|
|
duration: reduceMotion ? Duration.zero : motionDuration,
|
|
curve: Curves.easeInOutCubic,
|
|
scale: isEditing ? _searchCompactIconSize / idleIconSize : 1,
|
|
child: Icon(
|
|
BuzzIcons.search,
|
|
key: const Key('search-moving-icon'),
|
|
size: idleIconSize,
|
|
color: iconColor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|