From ef719c2425062c194ef87b6912f0a87f8bada3a9 Mon Sep 17 00:00:00 2001 From: Julien BIANCHI Date: Fri, 30 Oct 2026 10:00:42 +0100 Subject: [PATCH 1/2] feat(images): show the commit under the viewer's image in a pane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the web, a commit's image opens with the commit docked under it, and a button undocks it to see the whole picture. The native viewer showed the image alone. The viewer now has a sheet-pane under the image with the commit's author and its whole body, which scrolls in the pane when it's long. It opens drawn up, as the web docks it, and a drag takes it down to the author line and back up, in place of the web's dock button. The drag runs on the phone; only the height it comes to rest at goes back to PHP, and the pane stays there from one image to the next. The commit is kept as it was when the viewer opened, as its images already are. Refs #53. 🤖 Generated with Claude Code --- app/Devnull/Display.php | 9 +++++ .../Concerns/ShowsCommits.php | 28 ++++++++++++++-- .../native/partials/image-viewer.blade.php | 33 ++++++++++++++++++- tests/Feature/Screens/ImageViewerTest.php | 32 ++++++++++++++++++ 4 files changed, 99 insertions(+), 3 deletions(-) diff --git a/app/Devnull/Display.php b/app/Devnull/Display.php index 441cc2dc..d3261615 100644 --- a/app/Devnull/Display.php +++ b/app/Devnull/Display.php @@ -41,6 +41,15 @@ final class Display */ public const int ElbowLevel = 22; + /** + * The heights the pane under the image viewer's picture rests at, in + * points: its author line alone, and open on the whole commit, which + * scrolls in it when it's longer. + */ + public const int ViewerPaneClosed = 88; + + public const int ViewerPaneOpen = 360; + public static function relativeTime(?CarbonInterface $date, ?CarbonInterface $now = null): string { if ($date === null) { diff --git a/app/NativeComponents/Concerns/ShowsCommits.php b/app/NativeComponents/Concerns/ShowsCommits.php index d0fa47ea..fc33cdd6 100644 --- a/app/NativeComponents/Concerns/ShowsCommits.php +++ b/app/NativeComponents/Concerns/ShowsCommits.php @@ -72,6 +72,18 @@ trait ShowsCommits */ public array $viewerImages = []; + /** + * The commit being viewed, shown in the pane under its images, kept as it was when the viewer opened, as the images are. + */ + public ?Commit $viewerCommit = null; + + /** + * How far the pane under the viewer's image is drawn up: open, as the web + * docks the commit, until you drag it down to its author line. It stays + * where you left it from one image to the next. + */ + public float $viewerPaneDetent = Display::ViewerPaneOpen; + /** * Which of the viewer's images is showing. */ @@ -286,19 +298,22 @@ public function openLinkPreview(string $id): void * Opens a commit's image full-screen in the app (see partials/image-viewer), * from the one the row tapped: the image whole rather than cropped, pinch * zoom between its whole self and four times, a drag to pan, and the - * chevrons under it to move between the commit's other images. An image the + * chevrons under it to move between the commit's other images. The commit + * itself sits under the image in a pane, as the web docks it. An image the * screen isn't showing, or one at no web address, opens nothing, as it did * in the browser. */ public function openImage(string $id, int $index = 0): void { - $images = $this->shownCommit($id)?->images ?? []; + $commit = $this->shownCommit($id); + $images = $commit->images ?? []; $image = $images[$index] ?? null; if ($image === null || ! SiteLink::isWebUrl($image->url)) { return; } + $this->viewerCommit = $commit; $this->viewerImages = $images; $this->viewerIndex = $index; $this->restViewerImage(); @@ -325,6 +340,14 @@ public function viewPreviousImage(): void $this->showInViewedImage($this->viewerIndex - 1); } + /** + * The pane under the viewer's image come to rest at another height, which it's drawn at from now on. + */ + public function viewerPaneMoved(string $height): void + { + $this->viewerPaneDetent = (float) $height; + } + /** * The viewer's close, back or outside tap, however the phone reports it: * the viewer goes and forgets the image and what its gestures left behind, @@ -333,6 +356,7 @@ public function viewPreviousImage(): void public function closeImageViewer(): void { $this->viewerOpen = false; + $this->viewerCommit = null; $this->viewerImages = []; $this->viewerIndex = 0; $this->viewerZoom = null; diff --git a/resources/views/native/partials/image-viewer.blade.php b/resources/views/native/partials/image-viewer.blade.php index 91ad5232..c97bdcbb 100644 --- a/resources/views/native/partials/image-viewer.blade.php +++ b/resources/views/native/partials/image-viewer.blade.php @@ -11,13 +11,20 @@ fails, shows whatever its platform's loader shows meanwhile — iOS its spinner, Android nothing — since the image element tells the app nothing about either, there's nothing of the app's to draw here. + The commit sits under the image in a pane, as the web docks it beside its lightbox: its author, then the whole + commit, which scrolls in the pane when it's longer. A drag takes the pane down to its author line, to see more of + the picture, and back up again, as the web's dock button does; the drag runs on the phone, and only where it comes + to rest comes back (see ShowsCommits::viewerPaneMoved()). The picture keeps the room the pane takes when it's down, + so the chevrons stay above it. + Always in the tree, shown or not, like the sheet, so the phone can animate it away and say when it's gone. Included beside the sheet, so every screen that shows commits has it once. --}} @if ($viewerImages !== []) @php($image = $viewerImages[$viewerIndex]) - + + @endif + + @if ($viewerCommit) + @php($author = $viewerCommit->author) + + + + + + + @include('native.partials.rail-avatar', ['user' => $author, 'lane' => $viewerCommit->lane(), 'size' => 48]) + + {{ $author->name }} + {{ $author->handle() }} · {{ \App\Devnull\Display::relativeTime($viewerCommit->createdAt) }} + + + + + @include('native.partials.commit-body', ['commit' => $viewerCommit, 'focused' => true]) + + + + + @endif + @endif diff --git a/tests/Feature/Screens/ImageViewerTest.php b/tests/Feature/Screens/ImageViewerTest.php index 7b2056b3..4e623cd6 100644 --- a/tests/Feature/Screens/ImageViewerTest.php +++ b/tests/Feature/Screens/ImageViewerTest.php @@ -1,5 +1,6 @@ and(nodeByRef($screen, 'image-viewer')['props']['visible'])->toBeTrue() ->and($screen->instance()->sheetOpen)->toBeTrue(); }); + +it('shows the commit under the image in a pane, open, as the web docks it', function (string $platform) { + $screen = logWithImages(platform: $platform)->tap('image-i1'); + $pane = nodeByRef($screen, 'viewer-pane'); + + expect($pane['type'])->toBe('sheet_pane') + ->and($pane['props']['detents'])->toBe(Display::ViewerPaneClosed.','.Display::ViewerPaneOpen) + ->and($pane['props']['detent'])->toBe((float) Display::ViewerPaneOpen) + ->and(treeShows($pane, 'Local Dev'))->toBeTrue() + ->and(treeShows($pane, '@local_dev'))->toBeTrue() + ->and(treeShows($pane, 'Shipped the mobile app.'))->toBeTrue(); +})->with(['ios', 'android']); + +it('keeps the pane where you drag it, from one image to the next', function () { + $screen = logWithImages(2)->tap('image-i1'); + + $screen->call('viewerPaneMoved', (string) Display::ViewerPaneClosed)->tap('viewed-next'); + + expect(nodeByRef($screen, 'viewer-pane')['props']['detent'])->toBe((float) Display::ViewerPaneClosed); +}); + +it('forgets the commit with the image when the viewer closes', function () { + $screen = logWithImages()->tap('image-i1'); + + expect($screen->instance()->viewerCommit?->id)->toBe('c1'); + + $screen->dismissSheet('image-viewer'); + + expect($screen->instance()->viewerCommit)->toBeNull() + ->and(findNode($screen->tree(), fn (array $node): bool => ($node['ref'] ?? null) === 'viewer-pane'))->toBeNull(); +}); From 83e9928cb24236044fb6d28da4cd0b8308a40c2d Mon Sep 17 00:00:00 2001 From: Julien BIANCHI Date: Sat, 31 Oct 2026 13:13:10 +0100 Subject: [PATCH 2/2] feat(images): size the viewer's commit pane to the commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pane opened at a fixed 360pt, which left room under a short commit and hid the chevrons. It now uses sheet-pane's new `fit` detent, which sizes it to the commit, and drags down to the 88pt author line. The pane reports `fit` or a height when it comes to rest, and either is kept from one image to the next. With `fit` the pane keeps the commit pinned to its top when it's down, so it's cut off at the bottom instead of moving, which a bottom sheet with a `fit` detent didn't do. This needs NativePHP/mobile-ui's sheet-pane `fit` detent. Refs #53. 🤖 Generated with Claude Code --- app/Devnull/Display.php | 12 ++++++---- .../Concerns/ShowsCommits.php | 14 +++++------ .../native/partials/image-viewer.blade.php | 23 +++++++++---------- tests/Feature/Screens/ImageViewerTest.php | 19 +++++++++------ 4 files changed, 37 insertions(+), 31 deletions(-) diff --git a/app/Devnull/Display.php b/app/Devnull/Display.php index d3261615..b5c9c5a0 100644 --- a/app/Devnull/Display.php +++ b/app/Devnull/Display.php @@ -42,13 +42,15 @@ final class Display public const int ElbowLevel = 22; /** - * The heights the pane under the image viewer's picture rests at, in - * points: its author line alone, and open on the whole commit, which - * scrolls in it when it's longer. + * How far down the pane under the image viewer's picture can go, in + * points: its author line. Open, it's as tall as the commit it shows. */ - public const int ViewerPaneClosed = 88; + public const int ViewerPaneDown = 88; - public const int ViewerPaneOpen = 360; + /** + * The room the picture leaves under it for that pane down, in points, with the pane's inset, so the chevrons stay above it. + */ + public const int ViewerPaneRoom = 104; public static function relativeTime(?CarbonInterface $date, ?CarbonInterface $now = null): string { diff --git a/app/NativeComponents/Concerns/ShowsCommits.php b/app/NativeComponents/Concerns/ShowsCommits.php index fc33cdd6..22a421fb 100644 --- a/app/NativeComponents/Concerns/ShowsCommits.php +++ b/app/NativeComponents/Concerns/ShowsCommits.php @@ -78,11 +78,11 @@ trait ShowsCommits public ?Commit $viewerCommit = null; /** - * How far the pane under the viewer's image is drawn up: open, as the web - * docks the commit, until you drag it down to its author line. It stays - * where you left it from one image to the next. + * Where the pane under the viewer's image rests: `fit`, as tall as the + * commit, as the web docks it, or the height in points of its author line + * once you drag it down. It stays where you left it from one image to the next. */ - public float $viewerPaneDetent = Display::ViewerPaneOpen; + public float|string $viewerPaneDetent = 'fit'; /** * Which of the viewer's images is showing. @@ -341,11 +341,11 @@ public function viewPreviousImage(): void } /** - * The pane under the viewer's image come to rest at another height, which it's drawn at from now on. + * The pane under the viewer's image come to rest: `fit`, or a height in points. */ - public function viewerPaneMoved(string $height): void + public function viewerPaneMoved(string $detent): void { - $this->viewerPaneDetent = (float) $height; + $this->viewerPaneDetent = $detent === 'fit' ? 'fit' : (float) $detent; } /** diff --git a/resources/views/native/partials/image-viewer.blade.php b/resources/views/native/partials/image-viewer.blade.php index c97bdcbb..0c69bcbe 100644 --- a/resources/views/native/partials/image-viewer.blade.php +++ b/resources/views/native/partials/image-viewer.blade.php @@ -12,10 +12,11 @@ element tells the app nothing about either, there's nothing of the app's to draw here. The commit sits under the image in a pane, as the web docks it beside its lightbox: its author, then the whole - commit, which scrolls in the pane when it's longer. A drag takes the pane down to its author line, to see more of - the picture, and back up again, as the web's dock button does; the drag runs on the phone, and only where it comes - to rest comes back (see ShowsCommits::viewerPaneMoved()). The picture keeps the room the pane takes when it's down, - so the chevrons stay above it. + commit, the pane as tall as it. A drag takes the pane down to its author line, to see more of the picture, and back + up again, as the web's dock button does; the commit stays pinned to the top, cut off at the bottom. The drag runs on + the phone, and only where it comes to rest comes back (see ShowsCommits::viewerPaneMoved()). The pane is drawn in + the viewer itself, so the picture behind keeps its pinch and pan, and the picture keeps the room the pane takes when + it's down, so the chevrons stay above it. Always in the tree, shown or not, like the sheet, so the phone can animate it away and say when it's gone. Included beside the sheet, so every screen that shows commits has it once. @@ -24,7 +25,7 @@ @if ($viewerImages !== []) @php($image = $viewerImages[$viewerIndex]) - + author) - - + + @@ -74,11 +75,9 @@ class="w-full h-full" {{ $author->handle() }} · {{ \App\Devnull\Display::relativeTime($viewerCommit->createdAt) }} - - - @include('native.partials.commit-body', ['commit' => $viewerCommit, 'focused' => true]) - - + + @include('native.partials.commit-body', ['commit' => $viewerCommit, 'focused' => true]) + @endif diff --git a/tests/Feature/Screens/ImageViewerTest.php b/tests/Feature/Screens/ImageViewerTest.php index 4e623cd6..c12e9a9f 100644 --- a/tests/Feature/Screens/ImageViewerTest.php +++ b/tests/Feature/Screens/ImageViewerTest.php @@ -308,25 +308,30 @@ function imagedCommitImages(): array ->and($screen->instance()->sheetOpen)->toBeTrue(); }); -it('shows the commit under the image in a pane, open, as the web docks it', function (string $platform) { +it('shows the commit under the image in a pane as tall as it, as the web docks it', function (string $platform) { $screen = logWithImages(platform: $platform)->tap('image-i1'); $pane = nodeByRef($screen, 'viewer-pane'); expect($pane['type'])->toBe('sheet_pane') - ->and($pane['props']['detents'])->toBe(Display::ViewerPaneClosed.','.Display::ViewerPaneOpen) - ->and($pane['props']['detent'])->toBe((float) Display::ViewerPaneOpen) + ->and($pane['props'])->toMatchArray([ + 'detents' => Display::ViewerPaneDown.',fit', + 'detent' => 'fit', + ]) ->and(treeShows($pane, 'Local Dev'))->toBeTrue() ->and(treeShows($pane, '@local_dev'))->toBeTrue() ->and(treeShows($pane, 'Shipped the mobile app.'))->toBeTrue(); })->with(['ios', 'android']); -it('keeps the pane where you drag it, from one image to the next', function () { +it('keeps the pane where you drag it, from one image to the next', function (string $moved, float|string $kept) { $screen = logWithImages(2)->tap('image-i1'); - $screen->call('viewerPaneMoved', (string) Display::ViewerPaneClosed)->tap('viewed-next'); + $screen->call('viewerPaneMoved', $moved)->tap('viewed-next'); - expect(nodeByRef($screen, 'viewer-pane')['props']['detent'])->toBe((float) Display::ViewerPaneClosed); -}); + expect(nodeByRef($screen, 'viewer-pane')['props']['detent'])->toBe($kept); +})->with([ + 'down to its author' => [(string) Display::ViewerPaneDown, (float) Display::ViewerPaneDown], + 'back up to the commit' => ['fit', 'fit'], +]); it('forgets the commit with the image when the viewer closes', function () { $screen = logWithImages()->tap('image-i1');