mirror of
https://github.com/block/buzz.git
synced 2026-10-30 22:11:36 +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>
279 lines
9.1 KiB
Dart
279 lines
9.1 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
|
|
import '../theme/theme.dart';
|
|
|
|
enum ConcentricSurfaceCorners { all, bottom }
|
|
|
|
/// An iOS-native surface that adopts the system's concentric corners on iOS 26
|
|
/// and newer. Other platforms keep the normal Flutter shape.
|
|
class ConcentricSheetSurface extends HookWidget {
|
|
const ConcentricSheetSurface({
|
|
required this.child,
|
|
required this.enabled,
|
|
this.color,
|
|
this.backdropColor,
|
|
this.corners = ConcentricSurfaceCorners.all,
|
|
this.padding = const EdgeInsets.only(
|
|
left: Grid.xxs,
|
|
right: Grid.xxs,
|
|
bottom: Grid.xxs,
|
|
),
|
|
this.providesSheetSurface = true,
|
|
this.usesGlass = false,
|
|
this.glassTintColor,
|
|
this.minimumRadius = Radii.dialog,
|
|
this.contentClipRadius,
|
|
super.key,
|
|
});
|
|
|
|
final Widget child;
|
|
final bool enabled;
|
|
final Color? color;
|
|
final Color? backdropColor;
|
|
final ConcentricSurfaceCorners corners;
|
|
final EdgeInsetsGeometry padding;
|
|
final bool providesSheetSurface;
|
|
final bool usesGlass;
|
|
|
|
/// Optional tint applied to the native Liquid Glass material.
|
|
final Color? glassTintColor;
|
|
final double minimumRadius;
|
|
final double? contentClipRadius;
|
|
|
|
static bool providesSurfaceOf(BuildContext context) =>
|
|
context
|
|
.dependOnInheritedWidgetOfExactType<_ConcentricSheetSurfaceScope>()
|
|
?.providesSurface ??
|
|
false;
|
|
|
|
static const _surfaceChannel = MethodChannel('buzz/concentric_sheet_surface');
|
|
static const _nativeContentClipRadius = Radii.dialog * 2;
|
|
static const sheetTopRadius = _nativeContentClipRadius - Grid.xxs;
|
|
|
|
BorderRadius _borderRadius(double radius) => switch (corners) {
|
|
ConcentricSurfaceCorners.all => BorderRadius.circular(radius),
|
|
ConcentricSurfaceCorners.bottom => BorderRadius.vertical(
|
|
bottom: Radius.circular(radius),
|
|
),
|
|
};
|
|
|
|
Future<bool> _checkNativeSurfaceSupport() async {
|
|
try {
|
|
final supported = await _surfaceChannel.invokeMethod<bool>('isSupported');
|
|
return supported == true;
|
|
} on MissingPluginException {
|
|
// The registrar is unavailable, so retain the Flutter surface.
|
|
return false;
|
|
} on PlatformException {
|
|
// The native surface is optional; retain the Flutter surface on failure.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<void> _updateNativeSurfaceColors({
|
|
required MethodChannel channel,
|
|
required Color surfaceColor,
|
|
required Color? backdropColor,
|
|
}) async {
|
|
try {
|
|
await channel.invokeMethod<void>('updateColors', <String, Object?>{
|
|
'color': surfaceColor.toARGB32(),
|
|
'backdropColor': backdropColor?.toARGB32(),
|
|
'glassTintColor': glassTintColor?.toARGB32(),
|
|
});
|
|
} on MissingPluginException {
|
|
// The platform view may have been disposed while its theme was changing.
|
|
} on PlatformException {
|
|
// The native surface is optional; retain its last successfully sent color.
|
|
}
|
|
}
|
|
|
|
Future<void> _updateNativeSurfaceGeometry({
|
|
required MethodChannel channel,
|
|
required double minimumRadius,
|
|
required Brightness brightness,
|
|
}) async {
|
|
try {
|
|
await channel.invokeMethod<void>('updateGeometry', <String, Object>{
|
|
'minimumRadius': minimumRadius,
|
|
'brightness': brightness.name,
|
|
});
|
|
} on MissingPluginException {
|
|
// The platform view may have been disposed while its shape was changing.
|
|
} on PlatformException {
|
|
// The native surface is optional; retain its last successful geometry.
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final shouldCheckNativeSurface =
|
|
enabled && defaultTargetPlatform == TargetPlatform.iOS;
|
|
final supportFuture = useMemoized(
|
|
() => shouldCheckNativeSurface
|
|
? _checkNativeSurfaceSupport()
|
|
: Future<bool>.value(false),
|
|
[shouldCheckNativeSurface],
|
|
);
|
|
final nativeSurfaceSupported = useFuture(supportFuture).data ?? false;
|
|
final surfaceColor = color ?? context.colors.surface;
|
|
final brightness = context.theme.brightness;
|
|
final nativeSurfaceChannel = useState<MethodChannel?>(null);
|
|
useEffect(
|
|
() {
|
|
final channel = nativeSurfaceChannel.value;
|
|
if (!shouldCheckNativeSurface ||
|
|
!nativeSurfaceSupported ||
|
|
channel == null) {
|
|
return null;
|
|
}
|
|
unawaited(
|
|
_updateNativeSurfaceColors(
|
|
channel: channel,
|
|
surfaceColor: surfaceColor,
|
|
backdropColor: backdropColor,
|
|
),
|
|
);
|
|
return null;
|
|
},
|
|
[
|
|
shouldCheckNativeSurface,
|
|
nativeSurfaceSupported,
|
|
nativeSurfaceChannel.value,
|
|
surfaceColor,
|
|
backdropColor,
|
|
glassTintColor,
|
|
],
|
|
);
|
|
useEffect(
|
|
() {
|
|
final channel = nativeSurfaceChannel.value;
|
|
if (!shouldCheckNativeSurface ||
|
|
!nativeSurfaceSupported ||
|
|
channel == null) {
|
|
return null;
|
|
}
|
|
unawaited(
|
|
_updateNativeSurfaceGeometry(
|
|
channel: channel,
|
|
minimumRadius: minimumRadius,
|
|
brightness: brightness,
|
|
),
|
|
);
|
|
return null;
|
|
},
|
|
[
|
|
shouldCheckNativeSurface,
|
|
nativeSurfaceSupported,
|
|
nativeSurfaceChannel.value,
|
|
minimumRadius,
|
|
brightness,
|
|
],
|
|
);
|
|
|
|
if (!shouldCheckNativeSurface) {
|
|
return _ConcentricSheetSurfaceScope(providesSurface: false, child: child);
|
|
}
|
|
|
|
final fallbackBorderRadius = _borderRadius(minimumRadius);
|
|
|
|
return Padding(
|
|
padding: padding,
|
|
child: Stack(
|
|
children: [
|
|
if (nativeSurfaceSupported)
|
|
Positioned.fill(
|
|
child: ExcludeSemantics(
|
|
child: UiKitView(
|
|
viewType: 'buzz/concentric_sheet_surface',
|
|
// Keep the platform view in gesture arbitration. Excluding
|
|
// it with IgnorePointer leaves UIKit intercepting touches
|
|
// over Flutter controls after the native surface appears.
|
|
hitTestBehavior: PlatformViewHitTestBehavior.translucent,
|
|
onPlatformViewCreated: (viewId) {
|
|
nativeSurfaceChannel.value = MethodChannel(
|
|
'buzz/concentric_sheet_surface/$viewId',
|
|
);
|
|
},
|
|
creationParams: <String, Object>{
|
|
'color': surfaceColor.toARGB32(),
|
|
if (backdropColor case final color?)
|
|
'backdropColor': color.toARGB32(),
|
|
'minimumRadius': minimumRadius,
|
|
'corners': corners.name,
|
|
if (providesSheetSurface &&
|
|
corners == ConcentricSurfaceCorners.all)
|
|
'topRadius': sheetTopRadius,
|
|
'usesGlass': usesGlass,
|
|
if (glassTintColor case final tint?)
|
|
'glassTintColor': tint.toARGB32(),
|
|
'brightness': brightness.name,
|
|
},
|
|
creationParamsCodec: const StandardMessageCodec(),
|
|
),
|
|
),
|
|
)
|
|
else
|
|
Positioned.fill(
|
|
child: Material(
|
|
color: surfaceColor,
|
|
borderRadius: fallbackBorderRadius,
|
|
clipBehavior: Clip.antiAlias,
|
|
),
|
|
),
|
|
ClipRSuperellipse(
|
|
key: const ValueKey('concentric-sheet-content-clip'),
|
|
// UIKit's container-concentric corner resolves substantially
|
|
// larger than its minimum radius on an edge-inset iOS sheet. Keep
|
|
// the Flutter content inside that continuous outline; a 24pt
|
|
// circular clip still lets scrolling rows show through the native
|
|
// corner cutouts.
|
|
borderRadius:
|
|
nativeSurfaceSupported &&
|
|
providesSheetSurface &&
|
|
corners == ConcentricSurfaceCorners.all
|
|
? BorderRadius.vertical(
|
|
top: const Radius.circular(sheetTopRadius),
|
|
bottom: Radius.circular(
|
|
contentClipRadius ?? _nativeContentClipRadius,
|
|
),
|
|
)
|
|
: _borderRadius(
|
|
contentClipRadius ??
|
|
(nativeSurfaceSupported
|
|
? _nativeContentClipRadius
|
|
: minimumRadius),
|
|
),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: providesSheetSurface
|
|
? _ConcentricSheetSurfaceScope(
|
|
providesSurface: true,
|
|
child: child,
|
|
)
|
|
: child,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ConcentricSheetSurfaceScope extends InheritedWidget {
|
|
const _ConcentricSheetSurfaceScope({
|
|
required this.providesSurface,
|
|
required super.child,
|
|
});
|
|
|
|
final bool providesSurface;
|
|
|
|
@override
|
|
bool updateShouldNotify(_ConcentricSheetSurfaceScope oldWidget) =>
|
|
oldWidget.providesSurface != providesSurface;
|
|
}
|