Files
buzz/mobile/lib/shared/widgets/ios_navigation_metrics.dart
klopez4212 cb69ad9e4f Adapt iPhone Duo layouts, rail navigation, and native sheets (#8568)
Duo keeps conversations and Settings details open across closed, book
and flat postures. Book mode splits at the fold; flattening restores the
sidebar. Native rail controls, full-width navigation backdrops and
sheet-local safe areas keep content aligned. Ordinary iPhones remain
single-pane in portrait and landscape; tablet and Duo split layouts
remain enabled.

Includes the recovered preview's matching pane widths, Buzz empty-pane
mark, stable Settings chevrons, Bestie pane routing and Duo-only upward
New menu attachment. Current main's Activity redesign, Search content
fade and unread-navigation button are retained. Menu selection checks
sit beside their labels.

Shared mobile changes remain in scope: the native iOS status editor,
Appearance preview layout, avatar crop preservation, onboarding footer
layout, profile/invite sheet geometry and nested conversation
navigation. This PR is not exclusively Duo-gated. Nested navigator
observers preserve recording cancellation and navigation callbacks; crop
resizing preserves the selected image region. iOS 27.1 APIs remain
compile-time and runtime gated.

Validation on the reconciled source: Flutter analysis, Activity/Home
journeys, the full mobile test suite, file-size checks and Swift package
tests. The iPhone single-pane fix was also exercised on an iPhone 18 Pro
simulator before reconciliation. Fresh hosted CI and physical-device
confirmation of the reconciled build are separate and remain pending.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Tom Brow <tomb@squareup.com>
2026-11-12 19:00:58 +00:00

138 lines
4.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
/// UIKit's measured navigation geometry, with a first-frame bootstrap size.
class IosNavigationMetrics {
const IosNavigationMetrics({
this.compactHeight = 44,
this.largeTitleHeight = 52,
this.railCenterX,
this.railTopClearance,
this.windowSize,
this.bookDivision,
});
final double compactHeight;
final double largeTitleHeight;
final double? railCenterX;
final double? railTopClearance;
final Size? windowSize;
final Rect? bookDivision;
/// Aligns the shared rail with the status cluster in the reserved region.
double railCenter(Size size, double fallback) {
final center = windowSize == size ? railCenterX ?? fallback : fallback;
return center + (center < size.width / 2 ? 6 : -6);
}
static void updateRail(BuildContext context, Map<Object?, Object?> values) {
final notifier = _MetricsScope.of(context);
final center = (values['centerX'] as num?)?.toDouble();
final width = (values['windowWidth'] as num?)?.toDouble();
final height = (values['windowHeight'] as num?)?.toDouble();
if (notifier == null ||
center == null ||
width == null ||
height == null ||
!center.isFinite ||
!width.isFinite ||
!height.isFinite ||
center < 0 ||
center > width ||
width <= 0 ||
height <= 0) {
return;
}
final size = Size(width, height);
final rawTop = (values['topClearance'] as num?)?.toDouble();
final top =
rawTop != null && rawTop.isFinite && rawTop >= 0 && rawTop < height
? rawTop
: null;
final rawDivision = values['bookDivision'];
Rect? division;
if (rawDivision is List &&
rawDivision.length == 4 &&
rawDivision.every((value) => value is num && value.isFinite)) {
final bounds = rawDivision
.map((value) => (value as num).toDouble())
.toList();
final candidate = Rect.fromLTWH(
bounds[0],
bounds[1],
bounds[2],
bounds[3],
);
if (candidate.width >= 0 &&
candidate.left > 0 &&
candidate.right < width &&
candidate.top <= 1 &&
candidate.bottom >= height - 1) {
division = candidate;
}
}
final old = notifier.value;
if (old.railCenterX == center &&
old.railTopClearance == top &&
old.windowSize == size &&
old.bookDivision == division) {
return;
}
notifier.value = IosNavigationMetrics(
compactHeight: old.compactHeight,
largeTitleHeight: old.largeTitleHeight,
railCenterX: center,
railTopClearance: top,
windowSize: size,
bookDivision: division,
);
}
static IosNavigationMetrics of(BuildContext context) =>
_MetricsScope.of(context)?.value ?? const IosNavigationMetrics();
static void update(BuildContext context, Map<Object?, Object?> values) {
final notifier = _MetricsScope.of(context);
if (notifier == null) return;
final compact = (values['compactHeight'] as num?)?.toDouble();
final expanded = (values['expandedHeight'] as num?)?.toDouble();
if (compact == null || !compact.isFinite || compact <= 0) return;
final extra = expanded == null
? notifier.value.largeTitleHeight
: expanded - compact;
if (!extra.isFinite || extra < 0) return;
if ((notifier.value.compactHeight - compact).abs() < 0.1 &&
(notifier.value.largeTitleHeight - extra).abs() < 0.1) {
return;
}
notifier.value = IosNavigationMetrics(
compactHeight: compact,
largeTitleHeight: extra,
railCenterX: notifier.value.railCenterX,
railTopClearance: notifier.value.railTopClearance,
windowSize: notifier.value.windowSize,
bookDivision: notifier.value.bookDivision,
);
}
}
/// Shares measured UIKit spacing across routes and their body insets.
class IosNavigationMetricsHost extends HookWidget {
const IosNavigationMetricsHost({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
final metrics = useValueNotifier(const IosNavigationMetrics());
return _MetricsScope(notifier: metrics, child: child);
}
}
class _MetricsScope
extends InheritedNotifier<ValueNotifier<IosNavigationMetrics>> {
const _MetricsScope({required super.notifier, required super.child});
static ValueNotifier<IosNavigationMetrics>? of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<_MetricsScope>()?.notifier;
}