diff --git a/app/Devnull/Display.php b/app/Devnull/Display.php index 441cc2dc..b5c9c5a0 100644 --- a/app/Devnull/Display.php +++ b/app/Devnull/Display.php @@ -41,6 +41,17 @@ final class Display */ public const int ElbowLevel = 22; + /** + * 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 ViewerPaneDown = 88; + + /** + * 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 { if ($date === null) { diff --git a/app/NativeComponents/Concerns/ShowsCommits.php b/app/NativeComponents/Concerns/ShowsCommits.php index d0fa47ea..22a421fb 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; + + /** + * 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|string $viewerPaneDetent = 'fit'; + /** * 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: `fit`, or a height in points. + */ + public function viewerPaneMoved(string $detent): void + { + $this->viewerPaneDetent = $detent === 'fit' ? 'fit' : (float) $detent; + } + /** * 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..0c69bcbe 100644 --- a/resources/views/native/partials/image-viewer.blade.php +++ b/resources/views/native/partials/image-viewer.blade.php @@ -11,13 +11,21 @@ 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, 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. --}} @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..c12e9a9f 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 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'])->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 (string $moved, float|string $kept) { + $screen = logWithImages(2)->tap('image-i1'); + + $screen->call('viewerPaneMoved', $moved)->tap('viewed-next'); + + 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'); + + 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(); +});