From ccf9ac75dfd7379b4fe9eed74cc535b0038f7b24 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Mon, 26 Oct 2026 12:31:37 +0000 Subject: [PATCH] Draw skeleton-first tabs on Android only On iOS a tab you go back to opens as it was last drawn, straight from the tab bar. The skeleton frame then cut from those rows to a spinner, and back to the kept rows a frame or two later, which showed as a flicker on Notifications and Profile. iOS now opens on what it kept. Android keeps showing the tab you left until the new one draws, so the skeleton frame stays there as the sign your tap worked. Co-Authored-By: Claude Opus 5.5 --- app/NativeComponents/Concerns/DrawsSkeletonFirst.php | 12 +++++++++--- config/devnull.php | 7 ++++--- tests/Feature/Screens/NotificationsTest.php | 11 +++++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/app/NativeComponents/Concerns/DrawsSkeletonFirst.php b/app/NativeComponents/Concerns/DrawsSkeletonFirst.php index b541ff1..0c72f6b 100644 --- a/app/NativeComponents/Concerns/DrawsSkeletonFirst.php +++ b/app/NativeComponents/Concerns/DrawsSkeletonFirst.php @@ -3,15 +3,21 @@ namespace App\NativeComponents\Concerns; use App\Devnull\FirstFrame; +use Native\Mobile\Facades\System; /** - * A tab that opens on skeleton rows and fills in what it kept straight after. + * A tab that opens on skeleton rows and fills in what it kept straight after, + * on Android. * * Drawing a screen of kept rows took 2 to 3 seconds on a slow Android phone, * with nothing on screen to say the tap had worked. A first frame of skeleton * rows draws at once, and its kept rows and the refresh behind them follow as * soon as FirstFrame comes back from the background thread. * + * iOS opens on what it kept. It puts a tab back as it was last drawn the + * moment you tap it, so a skeleton frame there only cut from those rows to a + * spinner and back. + * * config('devnull.skeleton_first') turns it off, as phpunit.xml does, so a * test sees a screen as it fills in. */ @@ -24,11 +30,11 @@ trait DrawsSkeletonFirst /** * Shows what was kept, then calls $then (the screen's refresh), after a - * first frame of skeleton rows. + * first frame of skeleton rows on Android. */ protected function fillInAfterFirstFrame(string $then): void { - if (! config('devnull.skeleton_first')) { + if (! config('devnull.skeleton_first') || System::isIos()) { $this->fillIn($then); return; diff --git a/config/devnull.php b/config/devnull.php index 954bb29..0d31595 100644 --- a/config/devnull.php +++ b/config/devnull.php @@ -35,9 +35,10 @@ | Skeleton first |-------------------------------------------------------------------------- | - | A tab draws its first frame with skeleton rows only, so it shows as soon - | as you tap it, and fills in what it kept straight after. Off, it opens on - | what it kept, which tests rely on. + | On Android, a tab draws its first frame with skeleton rows only, so it + | shows as soon as you tap it, and fills in what it kept straight after. + | iOS, and every platform with this off, opens on what it kept, which + | tests rely on. | */ diff --git a/tests/Feature/Screens/NotificationsTest.php b/tests/Feature/Screens/NotificationsTest.php index 5848c3b..2b2b64c 100644 --- a/tests/Feature/Screens/NotificationsTest.php +++ b/tests/Feature/Screens/NotificationsTest.php @@ -504,6 +504,17 @@ function ringColour(TestableComponent $screen, string $id): ?string finishRefreshes($screen)->assertSet('skeletonFrame', false)->assertSee('followed you'); }); +it('opens on what it kept on iOS, which puts a tab back as it was last drawn, with no spinner first', function () { + config(['devnull.skeleton_first' => true]); + holdRefreshes(); + keep('notifications', inboxJson([notificationJson('followed')]), ['type' => null]); + + $screen = Native::visit('/notifications', platform: 'ios'); + + $screen->assertSet('skeletonFrame', false)->assertSee('followed you'); + expect(showsLoading(showingNotifications($screen)))->toBeFalse(); +}); + it('lists a group of followers when it is tapped, each with the same small follow back, and none for the group', function () { serveApi(function (Request $request) { return str_contains($request->url(), '/group')