Files
buzz/mobile/lib/shared/theme/buzz_theme.dart
klopez4212 39b8b2b6a6 feat(mobile): unify Messages navigation with Solo and Team filters (#8649)
## 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:

![Messages with All, Solo and Team filters and separate bottom
shortcuts](https://github.com/user-attachments/assets/562ddc3e-945b-4945-bd58-217a2c62b570)

Switcher layering during iteration (before → after):

| Before: filter edge showing through | After: Home hidden behind
switcher |
| --- | --- |
| ![Filter edge visible beside Switch
Community](https://github.com/user-attachments/assets/b32a0157-8b17-4f01-9a23-29b12ce3b1af)
| ![Clean Switch Community
screen](https://github.com/user-attachments/assets/222956e4-9989-4c8f-a162-65de45029845)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-29 14:27:22 +00:00

123 lines
5.1 KiB
Dart

import 'package:flutter/material.dart';
import 'accent_colors.dart';
import 'app_colors.dart';
import 'theme_extensions.dart';
/// Name of the first-party Buzz theme. Buzz reuses the GitHub Light palette for
/// every base color; the one thing that sets it apart is a branded gradient
/// painted across the app's top section. Mirrors desktop, where the same
/// gradient fills the sidebar canvas — see `data-buzz-sidebar` in
/// `desktop/src/shared/styles/globals/theme.css`.
const buzzThemeName = 'buzz';
/// Name of the dark counterpart, which reuses the GitHub Dark palette and the
/// dark-tuned gradient stops. Paired with [buzzThemeName] in `themePairs`, so
/// the two behave as a single "Buzz" choice under System mode.
const buzzDarkThemeName = 'buzz-dark';
/// Whether [themeName] is either half of the Buzz pair. Both halves enable the
/// gradient so System mode keeps it on across an OS light/dark switch.
bool isBuzzTheme(String themeName) =>
themeName == buzzThemeName || themeName == buzzDarkThemeName;
/// Whether the current widget tree is using the first-party Buzz treatment.
bool isBuzzThemeContext(BuildContext context) =>
Theme.of(context).extension<AppColors>()?.topSectionGradient != null;
/// Primary foreground for the mobile top navigation.
///
/// Every theme uses its own [ColorScheme.onSurface]. Buzz is the exception:
/// its desktop-matching top gradient needs a neutral black or white foreground
/// rather than the accent-derived color scheme foreground.
Color navigationPrimaryForeground(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
if (!isBuzzThemeContext(context)) return scheme.onSurface;
return scheme.brightness == Brightness.dark ? Colors.white : Colors.black;
}
/// Secondary label and placeholder foreground for the mobile top navigation.
Color navigationSecondaryForeground(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
if (!isBuzzThemeContext(context)) return scheme.onSurfaceVariant;
return navigationPrimaryForeground(context).withValues(alpha: 0.4);
}
/// Channel-section label and icon foreground for the mobile side navigation.
///
/// Section labels need more hierarchy than a placeholder. Buzz therefore uses
/// a stronger neutral over its gradient, while all other themes preserve their
/// established secondary foreground token.
Color navigationSectionForeground(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
if (!isBuzzThemeContext(context)) return scheme.onSurfaceVariant;
return navigationPrimaryForeground(context).withValues(alpha: 0.8);
}
/// Search-field surface for the mobile top navigation.
Color navigationSearchSurface(BuildContext context) {
return context.controlSurface;
}
/// A low-contrast navigation divider derived from the active theme foreground.
Color navigationDivider(BuildContext context, double opacity) =>
navigationPrimaryForeground(context).withValues(alpha: opacity);
/// Buzz renders with its fixed neutral foreground while preserving the stored
/// wire accent so the user's choice returns on another theme.
int effectiveAccentIndex(String themeName, String storedAccent) {
if (isBuzzTheme(themeName)) return neutralAccentIndex;
return accentIndexForWireValue(storedAccent) ?? defaultAccentIndex;
}
/// Gradient stops, matching desktop's `--buzz-gradient-*` custom properties.
const _lightTop = Color(0xFFE6E6B6);
const _lightBottom = Color(0xFFC4D0DA);
const _darkTop = Color(0xFF4A4616);
const _darkBottom = Color(0xFF0A1423);
/// The Buzz gradient for the app's top section, or null when [themeName] is not
/// a Buzz theme — in which case the section keeps its default frosted fill.
///
/// The stops are fully opaque: under Buzz the color replaces the frosted
/// treatment rather than tinting it, matching desktop's solid sidebar canvas.
///
/// [brightness] comes from the applied color scheme rather than the theme name,
/// so System mode picks the right stops as the OS switches.
LinearGradient? buzzTopSectionGradient(
String themeName,
Brightness brightness,
) {
if (!isBuzzTheme(themeName)) return null;
final isDark = brightness == Brightness.dark;
return LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
isDark ? _darkTop : _lightTop,
isDark ? _darkBottom : _lightBottom,
],
);
}
/// Desktop navigation ink distinguishes read destinations from unread labels.
Color channelNavigationForeground(BuildContext context, {bool unread = false}) {
if (unread || Theme.of(context).brightness != Brightness.dark) {
return navigationPrimaryForeground(context);
}
return Theme.of(context).colorScheme.onSurfaceVariant;
}
/// Metadata ink used by Home section headings and muted destinations.
Color navigationMetadataForeground(BuildContext context) =>
Theme.of(context).brightness == Brightness.dark
? const Color(0xFFA4A4A4)
: const Color(0xFF5F5F5F);
/// Quieter ink for section disclosure and overflow controls.
Color navigationAccessoryForeground(BuildContext context) =>
navigationMetadataForeground(
context,
).withValues(alpha: MediaQuery.highContrastOf(context) ? 1 : 0.8);