Skip to content

Commit 8746bfe

Browse files
authored
Polish iOS header glass and scroll-edge blur (#8097)
Wrap channel and DM titles in native glass and remove the temporary-channel clock. Apply a soft status-area fade across iPhone navigation, and fade the large title with scroll progress and fade in the centered title, with Reduce Motion support. Validation: mobile formatting/analysis and the full mobile suite passed. Native iOS 26/27 simulator checks covered actual text opacity, header resizing, quick reversal, and restored heading position; signed iPhone build, installation, and launch succeeded. `just ci` was blocked by disk exhaustion during Rust compilation. --------- Signed-off-by: kenny lopez <[email protected]>
1 parent 46e6020 commit 8746bfe

7 files changed

Lines changed: 483 additions & 130 deletions

File tree

‎mobile/ios/Runner/IosNavigationBar.swift‎

Lines changed: 172 additions & 62 deletions
Large diffs are not rendered by default.

‎mobile/ios/RunnerTests/RunnerTests.swift‎

Lines changed: 255 additions & 55 deletions
Large diffs are not rendered by default.

‎mobile/lib/features/channels/channel_detail_page.dart‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,11 @@ class ChannelDetailPage extends HookConsumerWidget {
597597
}
598598
}
599599

600+
final retention = ephemeralChannelDisplay(resolvedChannel);
601+
final nativeHeaderSubtitle = isOneToOneDm
602+
? dmHeader?.presenceLabel
603+
: headerMemberLabel;
604+
600605
final timelineMessages = resolvedChannel.isForum
601606
? <TimelineMessage>[]
602607
: formatTimeline(
@@ -616,18 +621,22 @@ class ChannelDetailPage extends HookConsumerWidget {
616621
appBar: FrostedAppBar(
617622
alwaysFrosted: true,
618623
nativeViewSuppressed: messageActionBackdropActive,
619-
nativeEphemeralLabel: ephemeralChannelDisplay(
620-
resolvedChannel,
621-
)?.tooltipLabel,
622624
nativeTitle:
623625
dmHeader?.label ??
624626
resolveDmChannelDisplayLabel(
625627
resolvedChannel,
626628
currentPubkey: currentPubkey,
627629
),
628-
nativeSubtitle: isOneToOneDm
629-
? dmHeader?.presenceLabel
630-
: headerMemberLabel,
630+
nativeSubtitle: retention == null
631+
? nativeHeaderSubtitle
632+
: [
633+
'Temporary',
634+
if (retention.detailLabel != null) retention.detailLabel!,
635+
if (nativeHeaderSubtitle != null &&
636+
nativeHeaderSubtitle.isNotEmpty)
637+
nativeHeaderSubtitle,
638+
].join(' · '),
639+
nativeEphemeralLabel: retention?.tooltipLabel,
631640
nativeTitlePresenceColor: switch (isOneToOneDm
632641
? dmHeader?.presence
633642
: null) {

‎mobile/lib/shared/widgets/frosted_app_bar.dart‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ class FrostedAppBar extends StatelessWidget {
144144
/// surrounding scroll scope reports content beneath the controls.
145145
final bool frosted;
146146

147-
/// Keeps the Flutter backdrop and divider stable regardless of scroll state.
147+
/// Keeps the backdrop stable regardless of scroll state on both platforms.
148148
/// Conversation headers use this while their timeline and composer resize.
149149
final bool alwaysFrosted;
150150

@@ -285,6 +285,7 @@ class FrostedAppBar extends StatelessWidget {
285285
titlePresenceColor: nativeTitlePresenceColor,
286286
onTitlePressed: onNativeTitlePressed,
287287
largeTitle: nativeLargeTitle,
288+
alwaysFrosted: alwaysFrosted,
288289
foregroundColor: iconColor,
289290
leading: nativeLeading ?? _nativeAction(leading),
290291
onBack:

‎mobile/lib/shared/widgets/ios_navigation_bar.dart‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class IosNavigationBar extends HookConsumerWidget {
116116
this.titlePresenceColor,
117117
this.onTitlePressed,
118118
this.largeTitle = false,
119+
this.alwaysFrosted = false,
119120
this.leading,
120121
this.actions = const [],
121122
this.onBack,
@@ -132,6 +133,9 @@ class IosNavigationBar extends HookConsumerWidget {
132133
final Color? titlePresenceColor;
133134
final VoidCallback? onTitlePressed;
134135
final bool largeTitle;
136+
137+
/// Keeps the navigation backdrop visible before the first scroll update.
138+
final bool alwaysFrosted;
135139
final IosNavigationAction? leading;
136140
final List<IosNavigationAction> actions;
137141
final VoidCallback? onBack;
@@ -229,6 +233,7 @@ class IosNavigationBar extends HookConsumerWidget {
229233
'titlePresenceColor': titlePresenceColor?.toARGB32(),
230234
'titleEnabled': onTitlePressed != null,
231235
'largeTitle': largeTitle,
236+
'alwaysFrosted': alwaysFrosted,
232237
'back': onBack != null,
233238
'leading': leading == null ? null : encodeAction(leading!, 'leading'),
234239
'actions': [

‎mobile/test/features/channels/channel_detail_page_test.dart‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10128,7 +10128,7 @@ void main() {
1012810128
});
1012910129

1013010130
for (final dm in [false, true]) {
10131-
testWidgets('native ephemeral header retains expiry disclosure dm=$dm', (
10131+
testWidgets('native temporary title updates retention text dm=$dm', (
1013210132
tester,
1013310133
) async {
1013410134
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
@@ -10168,8 +10168,9 @@ void main() {
1016810168
Map payload = view.creationParams! as Map;
1016910169
expect(
1017010170
payload['ephemeralLabel'],
10171-
'Ephemeral channel. Cleans up after 1 hour of inactivity.',
10171+
contains('after 1 hour of inactivity'),
1017210172
);
10173+
expect(payload['subtitle'], startsWith('Temporary · 1h TTL'));
1017310174
const bridge = MethodChannel('buzz/ios_navigation_bar/297');
1017410175
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(bridge, (
1017510176
call,
@@ -10191,14 +10192,13 @@ void main() {
1019110192
);
1019210193
container.invalidate(channelDetailsProvider(_channelId));
1019310194
await tester.pumpAndSettle();
10194-
expect(
10195-
payload['ephemeralLabel'],
10196-
'Ephemeral channel. Cleanup is due now.',
10197-
);
10195+
expect(payload['ephemeralLabel'], contains('Cleanup is due now'));
10196+
expect(payload['subtitle'], startsWith('Temporary · Cleanup due'));
1019810197
current = conversation();
1019910198
container.invalidate(channelDetailsProvider(_channelId));
1020010199
await tester.pumpAndSettle();
1020110200
expect(payload['ephemeralLabel'], isNull);
10201+
expect(payload['subtitle'], isNot(contains('Temporary')));
1020210202
await tester.pumpWidget(const SizedBox());
1020310203
debugDefaultTargetPlatformOverride = null;
1020410204
});

‎mobile/test/shared/widgets/frosted_app_bar_test.dart‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,34 @@ import 'package:flutter_test/flutter_test.dart';
1010
import 'package:hooks_riverpod/hooks_riverpod.dart';
1111

1212
void main() {
13+
testWidgets('iOS conversations request a backdrop before scrolling', (
14+
tester,
15+
) async {
16+
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
17+
addTearDown(() => debugDefaultTargetPlatformOverride = null);
18+
await tester.pumpWidget(
19+
ProviderScope(
20+
child: MaterialApp(
21+
theme: AppTheme.light(),
22+
home: const Stack(
23+
children: [
24+
FrostedAppBar(
25+
title: Text('Alice'),
26+
nativeSubtitle: 'Online',
27+
alwaysFrosted: true,
28+
),
29+
],
30+
),
31+
),
32+
),
33+
);
34+
final view = tester.widget<UiKitView>(find.byType(UiKitView));
35+
expect(view.creationParams, containsPair('alwaysFrosted', true));
36+
expect(view.creationParams, containsPair('subtitle', 'Online'));
37+
await tester.pumpWidget(const SizedBox());
38+
debugDefaultTargetPlatformOverride = null;
39+
});
40+
1341
testWidgets(
1442
'conversation backdrop stays mounted across scroll and keyboard changes',
1543
(tester) async {

0 commit comments

Comments
 (0)