mirror of
https://github.com/block/buzz.git
synced 2026-11-13 21:01:53 +00:00
## Summary Polish mobile Home and Solo: group Search/Inbox beside a separate profile avatar, move Inbox and Settings to bottom-up pages with Close controls, and place tabs opposite the plus/compose action. Morph plus to compose clockwise when entering Solo and reverse counterclockwise when returning Home, with a directional quarter-turn of the icon inside a level button and a subtle scale pulse, respecting reduced motion. Align Search’s Close control with Inbox, simplify its placeholder, and initialize selected filter contrast correctly. Collapse the plus menu when leaving Home. Center Solo’s native-glass New chat empty state and keep it stationary behind the keyboard. Standardize Tabler icons and neutral backgrounds, retain SF Back/Close on iOS, use native Liquid Glass headers, tab container, and actions (solid grey selected tab, slightly darker on iOS) on iOS and black actions on Android, and smooth the expanded menu corners. Repair native header safe-area/return layout and compose movement after cancellation. ### Related issue None found for this UI refinement; follows #8337. ### Testing - Formatting, analysis, full mobile suite: 3,487 passed, 4 skipped; 3 push-configuration tests passed. File-size checks passed. - Base UI built and installed on Pixel and iPhone in place. Pixel navigation/New chat cancellation checked. Subsequent glass and rotation refinements are previewed on iPhone; motion feel awaits human confirmation. - Draft pending human confirmation of the final Settings entrance/return behavior on iPhone. ### Pixel snapshots | Solo | Expanded actions | | --- | --- | | <img src="https://raw.githubusercontent.com/block/buzz/16dc0f8af971e63d788fee20974997fab925dec9/docs/screenshots/mobile-home-solo/pixel-solo.png" width="280" alt="Solo empty state with New chat" /> | <img src="https://raw.githubusercontent.com/block/buzz/16dc0f8af971e63d788fee20974997fab925dec9/docs/screenshots/mobile-home-solo/pixel-actions.png" width="280" alt="Expanded channel actions with smooth corners" /> | --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
221 lines
7.3 KiB
Dart
221 lines
7.3 KiB
Dart
part of '../activity_page.dart';
|
|
|
|
const _filterLabels = <InboxFilter, String>{
|
|
InboxFilter.all: 'All',
|
|
InboxFilter.mention: 'Mentions',
|
|
InboxFilter.thread: 'Threads',
|
|
InboxFilter.needsAction: 'Needs Action',
|
|
InboxFilter.activity: 'Activity',
|
|
InboxFilter.agentActivity: 'Agents',
|
|
InboxFilter.reminders: 'Reminders',
|
|
InboxFilter.drafts: 'Drafts',
|
|
};
|
|
|
|
class _ActivityActionsPill extends StatelessWidget {
|
|
final InboxFilter filter;
|
|
final bool unreadOnly;
|
|
final int unreadCount;
|
|
final ValueChanged<InboxFilter> onFilterChanged;
|
|
final ValueChanged<bool> onUnreadOnlyChanged;
|
|
final VoidCallback onMarkAllRead;
|
|
|
|
const _ActivityActionsPill({
|
|
required this.filter,
|
|
required this.unreadOnly,
|
|
required this.unreadCount,
|
|
required this.onFilterChanged,
|
|
required this.onUnreadOnlyChanged,
|
|
required this.onMarkAllRead,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) => ClipRRect(
|
|
borderRadius: BorderRadius.circular(Radii.full),
|
|
child: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
color: context.controlSurface,
|
|
borderRadius: BorderRadius.circular(Radii.full),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: Grid.quarter),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_FilterMenuButton(filter: filter, onChanged: onFilterChanged),
|
|
_InboxOptionsButton(
|
|
unreadOnly: unreadOnly,
|
|
unreadCount: unreadCount,
|
|
onUnreadOnlyChanged: onUnreadOnlyChanged,
|
|
onMarkAllRead: onMarkAllRead,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Compact filter dropdown replacing the old chip rail — mirrors desktop's
|
|
/// inbox filter menu (`FILTER_OPTIONS`).
|
|
class _FilterMenuButton extends StatelessWidget {
|
|
final InboxFilter filter;
|
|
final ValueChanged<InboxFilter> onChanged;
|
|
|
|
const _FilterMenuButton({required this.filter, required this.onChanged});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Builder(
|
|
builder: (buttonContext) => InkWell(
|
|
key: const ValueKey('activity-filter-menu'),
|
|
borderRadius: BorderRadius.circular(Radii.md),
|
|
onTap: () async {
|
|
final selected = await showAnchoredPopover<InboxFilter>(
|
|
context: buttonContext,
|
|
width: 240,
|
|
alignment: AnchoredPopoverAlignment.start,
|
|
offset: const Offset(0, Grid.half),
|
|
menuPadding: const EdgeInsets.symmetric(vertical: Grid.half),
|
|
surfaceKey: const ValueKey('activity-filter-popover'),
|
|
items: [
|
|
for (final entry in _filterLabels.entries)
|
|
PopupMenuItem(
|
|
value: entry.key,
|
|
height: Grid.xl,
|
|
padding: const EdgeInsets.symmetric(horizontal: Grid.twelve),
|
|
child: Row(
|
|
children: [
|
|
SizedBox(
|
|
width: Grid.sm,
|
|
child: entry.key == filter
|
|
? Icon(
|
|
BuzzIcons.check,
|
|
size: 16,
|
|
color: context.colors.primary,
|
|
)
|
|
: null,
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
entry.value,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: context.textTheme.labelLarge?.copyWith(
|
|
color: context.colors.onSurface,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
if (buttonContext.mounted && selected != null) onChanged(selected);
|
|
},
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(minHeight: Grid.xl),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: Grid.xxs),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
_filterLabels[filter]!,
|
|
style: context.textTheme.labelLarge?.copyWith(
|
|
color: navigationPrimaryForeground(context),
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(width: Grid.quarter),
|
|
Icon(
|
|
BuzzIcons.chevronDown,
|
|
size: 16,
|
|
color: navigationPrimaryForeground(context),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Overflow menu with the unread-only toggle and mark-all-read, mirroring
|
|
/// desktop's inbox options popover.
|
|
class _InboxOptionsButton extends StatelessWidget {
|
|
final bool unreadOnly;
|
|
final int unreadCount;
|
|
final ValueChanged<bool> onUnreadOnlyChanged;
|
|
final VoidCallback onMarkAllRead;
|
|
|
|
const _InboxOptionsButton({
|
|
required this.unreadOnly,
|
|
required this.unreadCount,
|
|
required this.onUnreadOnlyChanged,
|
|
required this.onMarkAllRead,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Builder(
|
|
builder: (buttonContext) => IconButton(
|
|
key: const ValueKey('activity-options-menu'),
|
|
tooltip: 'Inbox options',
|
|
color: navigationPrimaryForeground(context),
|
|
padding: const EdgeInsets.symmetric(horizontal: Grid.xxs),
|
|
constraints: const BoxConstraints.tightFor(
|
|
width: Grid.xl,
|
|
height: Grid.xl,
|
|
),
|
|
icon: const Icon(BuzzIcons.ellipsis, size: 20),
|
|
onPressed: () async {
|
|
final selected = await showAnchoredPopover<String>(
|
|
context: buttonContext,
|
|
width: 216,
|
|
alignment: AnchoredPopoverAlignment.end,
|
|
surfaceKey: const ValueKey('activity-options-popover'),
|
|
items: [
|
|
PopupMenuItem(
|
|
value: 'unread-only',
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(unreadOnly ? 'Show all' : 'Show unread'),
|
|
),
|
|
if (unreadOnly)
|
|
Icon(
|
|
BuzzIcons.check,
|
|
size: 16,
|
|
color: context.colors.primary,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
PopupMenuItem(
|
|
value: 'mark-all-read',
|
|
enabled: unreadCount > 0,
|
|
child: Row(
|
|
children: [
|
|
const Expanded(child: Text('Mark all as read')),
|
|
if (unreadCount > 0)
|
|
Text(
|
|
'$unreadCount',
|
|
style: context.textTheme.labelSmall?.copyWith(
|
|
color: context.colors.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
if (!buttonContext.mounted || selected == null) return;
|
|
if (selected == 'unread-only') onUnreadOnlyChanged(!unreadOnly);
|
|
if (selected == 'mark-all-read') onMarkAllRead();
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|