Skip to content
Merged
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ To make that instance work with the app on your phone, create a tunnel (ngrok, C

These are the next things we want in the app. If you'd like to take one on, read [CONTRIBUTING.md](CONTRIBUTING.md) first.

- A native image viewer, instead of opening images in a browser sheet
- Styling for livestreams
- Search
- A notifications centre that matches the web
Expand Down
13 changes: 13 additions & 0 deletions app/Devnull/Data/CommitImage.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,17 @@ public function ratio(): float

return round(max(self::TallestRatio, $this->width / $this->height), 4);
}

/**
* Width over height as the image really is, uncapped, for drawing it whole;
* null when devnull didn't send its size.
*/
public function naturalRatio(): ?float
{
if ($this->width === 0 || $this->height === 0) {
return null;
}

return round($this->width / $this->height, 4);
}
}
3 changes: 3 additions & 0 deletions app/Icons/Lucide.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ enum Lucide
case Trash2;
case CornerDownRight;
case ChevronRight;
case ChevronLeft;
case ChevronDown;
case ArrowUpRight;
case ArrowUp;
Expand Down Expand Up @@ -192,6 +193,7 @@ public function ios(bool $filled = false): Ios|string
return $this->custom($filled) ?? match ($this) {
self::CornerDownRight => Ios::ArrowTurnDownRight,
self::ChevronRight => Ios::ChevronRight,
self::ChevronLeft => Ios::ChevronLeft,
self::ChevronDown => Ios::ChevronDown,
self::ArrowUpRight => Ios::ArrowUpRight,
self::Pin => Ios::Pin,
Expand All @@ -209,6 +211,7 @@ public function android(bool $filled = false): AndroidSymbol|string
return $this->custom($filled) ?? match ($this) {
self::CornerDownRight => AndroidOutlined::SubdirectoryArrowRight,
self::ChevronRight => AndroidOutlined::ChevronRight,
self::ChevronLeft => AndroidOutlined::ChevronLeft,
self::ChevronDown => AndroidOutlined::ExpandMore,
self::ArrowUpRight => AndroidOutlined::ArrowOutward,
self::Pin => AndroidOutlined::PushPin,
Expand Down
133 changes: 129 additions & 4 deletions app/NativeComponents/Concerns/ShowsCommits.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
use App\Devnull\Account;
use App\Devnull\Client;
use App\Devnull\Data\Commit;
use App\Devnull\Data\CommitImage;
use App\Devnull\Data\Pasteis;
use App\Devnull\Data\User;
use App\Devnull\ErrorCode;
use App\Devnull\Exceptions\ApiException;
use App\Devnull\ResponseCache;
use Closure;
use Native\Mobile\Attributes\On;
use Native\Mobile\Edge\SharedValue;
use Native\Mobile\Events\Alert\ButtonPressed;
use Native\Mobile\Facades\Browser;
use Native\Mobile\Facades\Dialog;
Expand All @@ -21,8 +23,9 @@
* The actions under a commit: the star, the pastel in pastéis mode, Quote,
* Reply, and the More menu's Save commit, Share link, Pin to profile, Report
* commit and Delete commit (Quote and Report open a sheet, see
* OpensCommitSheet), plus opening its link preview or images in the in-app
* browser, "Show more" on a long one, and voting on its poll (see
* OpensCommitSheet), plus opening its link preview in the in-app browser, its
* images full-screen in the app (see partials/image-viewer), "Show more" on a
* long one, and voting on its poll (see
* VotesOnPolls). Taps name the commit rather than a URL, so a URL is never
* rebuilt from an attribute.
*
Expand All @@ -49,6 +52,37 @@ trait ShowsCommits
*/
public ?string $deleting = null;

/**
* Whether the image viewer is up, over everything else on the screen,
* sheet included.
*/
public bool $viewerOpen = false;

/**
* The images of the commit being viewed, as they were when it opened, so
* the viewer keeps what it shows while it slides away even if the commit
* leaves the screen.
*
* @var list<CommitImage>
*/
public array $viewerImages = [];

/**
* Which of the viewer's images is showing.
*/
public int $viewerIndex = 0;

/**
* The zoom and pan of the image being viewed, driven on the phone by the
* viewer's gesture area. Fresh ones on every open and every move between
* images, so each starts at rest; nulled on close.
*/
public ?SharedValue $viewerZoom = null;

public ?SharedValue $viewerPanX = null;

public ?SharedValue $viewerPanY = null;

/**
* Every commit the screen is showing.
*
Expand Down Expand Up @@ -194,9 +228,62 @@ public function openLinkPreview(string $id): void
$this->openInBrowser($this->shownCommit($id)?->linkPreview?->url);
}

/**
* 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
* 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
{
$this->openInBrowser(($this->shownCommit($id)?->images[$index] ?? null)?->url);
$images = $this->shownCommit($id)?->images ?? [];
$image = $images[$index] ?? null;

if ($image === null || ! $this->isWebUrl($image->url)) {
return;
}

$this->viewerImages = $images;
$this->viewerIndex = $index;
$this->restViewerImage();
$this->viewerOpen = true;
}

/**
* The chevron under the viewer: the commit's next image, at rest again. At
* the last image there's no chevron to press, and a next at no web address
* goes nowhere, as an image at one never opens.
*/
public function viewNextImage(): void
{
$this->showInViewedImage($this->viewerIndex + 1);
}

/**
* The chevron under the viewer: the commit's previous image, at rest
* again. At the first image there's no chevron to press, and a previous at
* no web address goes nowhere either.
*/
public function viewPreviousImage(): void
{
$this->showInViewedImage($this->viewerIndex - 1);
}

/**
* 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,
* so the next one opens as this one did.
*/
public function closeImageViewer(): void
{
$this->viewerOpen = false;
$this->viewerImages = [];
$this->viewerIndex = 0;
$this->viewerZoom = null;
$this->viewerPanX = null;
$this->viewerPanY = null;
}

/**
Expand Down Expand Up @@ -452,8 +539,46 @@ private function shownCommit(string $id): ?Commit

private function openInBrowser(?string $url): void
{
if ($url !== null && in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'], true)) {
if ($this->isWebUrl($url)) {
Browser::inApp($url);
}
}

/**
* Only an http or https address is opened, in the browser or the viewer, so
* nothing else a row might carry can take the phone somewhere unexpected.
*/
private function isWebUrl(?string $url): bool
{
return $url !== null && in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'], true);
}

/**
* Puts the viewer's image back at rest: fresh shared values, which the
* phone seeds at one and none, so what the last image's gestures left
* behind can't carry over to it.
*/
private function restViewerImage(): void
{
$this->viewerZoom = SharedValue::make(1.0);
$this->viewerPanX = SharedValue::make();
$this->viewerPanY = SharedValue::make();
}

/**
* Moves the viewer to one of its commit's images by its place among them,
* or nowhere when there's no image there or it's at no web address, so the
* viewer never comes to show one the phone shouldn't be asked for.
*/
private function showInViewedImage(int $index): void
{
$image = $this->viewerImages[$index] ?? null;

if (! $this->viewerOpen || $image === null || ! $this->isWebUrl($image->url)) {
return;
}

$this->viewerIndex = $index;
$this->restViewerImage();
}
}
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@
"laravel/boost": "^2.0",
"laravel/framework": "^13.0",
"laravel/tinker": "^3.0",
"nativephp/mobile": "dev-main as 4.6.0",
"nativephp/mobile": "dev-feat/gesture-area-bounded as 4.6.0",
"nativephp/mobile-browser": "^1.2",
"nativephp/mobile-camera": "^1.1",
"nativephp/mobile-secure-storage": "^1.1",
"nativephp/mobile-share": "^1.1",
"nativephp/mobile-ui": "dev-main as 0.8.0",
"nativephp/mobile-ui": "dev-feat/gesture-area-bounded as 0.8.0",
"s2br/nativephp-mobile-splashscreen": "^1.4"
},
"require-dev": {
Expand Down
32 changes: 15 additions & 17 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions resources/views/native/partials/commit-sheet.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,5 @@ class="w-full"
@endif
@endif
</native:bottom-sheet>

@include('native.partials.image-viewer')
Loading
Loading