diff --git a/app/Devnull/Data/Commit.php b/app/Devnull/Data/Commit.php index c4138a5..6c97c4a 100644 --- a/app/Devnull/Data/Commit.php +++ b/app/Devnull/Data/Commit.php @@ -171,6 +171,16 @@ public function withReactionsFrom(self $fresh): self ]); } + /** + * Whether a body is long enough to commit and short enough for devnull, once trimmed. + */ + public static function fits(string $body): bool + { + $length = mb_strlen(trim($body)); + + return $length >= self::MinLength && $length <= self::MaxLength; + } + /** * Whether a list would show only the start of a body, as the web's composer * works it out: more than PreviewLength characters or PreviewLines lines, diff --git a/app/NativeComponents/Concerns/DraftsIssues.php b/app/NativeComponents/Concerns/DraftsIssues.php index b3779f3..5f46a39 100644 --- a/app/NativeComponents/Concerns/DraftsIssues.php +++ b/app/NativeComponents/Concerns/DraftsIssues.php @@ -123,7 +123,7 @@ public function openIssueFromSheet(): void $command = $this->sheetIssue === null ? null : IssueCommand::tryFrom($this->sheetIssue); $body = trim($this->sheetBody); - if ($command === null || $this->openingIssue || ! $this->canDraftIssue() || mb_strlen($body) < Commit::MinLength || mb_strlen($body) > Commit::MaxLength) { + if ($command === null || $this->openingIssue || ! $this->canDraftIssue() || ! Commit::fits($body)) { return; } diff --git a/app/NativeComponents/Concerns/OpensCommitSheet.php b/app/NativeComponents/Concerns/OpensCommitSheet.php index 6ae0ff2..30b5282 100644 --- a/app/NativeComponents/Concerns/OpensCommitSheet.php +++ b/app/NativeComponents/Concerns/OpensCommitSheet.php @@ -180,7 +180,7 @@ public function commitFromSheet(): void $stashName = $this->sheetStashName(); $body = trim($this->sheetBody); - if ($stashName === null || $this->sheetIssue !== null || mb_strlen($body) < Commit::MinLength || mb_strlen($body) > Commit::MaxLength) { + if ($stashName === null || $this->sheetIssue !== null || ! Commit::fits($body)) { return; } @@ -196,7 +196,8 @@ public function commitFromSheet(): void } /** - * Done: the sheet goes. + * Done, after a report, and Cancel, in the composer's bar: the sheet goes. A draft in it stays stashed, as when + * it's swiped away, so this mustn't forget it. */ public function hideCommitSheet(): void { diff --git a/resources/views/native/partials/commit-sheet.blade.php b/resources/views/native/partials/commit-sheet.blade.php index 5d97e7e..4e23606 100644 --- a/resources/views/native/partials/commit-sheet.blade.php +++ b/resources/views/native/partials/commit-sheet.blade.php @@ -1,9 +1,11 @@ {{-- - The sheet under a commit (see OpensCommitSheet). Quote opens it as a composer: the field, focused, with the commit - being quoted under it as a static card, then the counter and Commit, which waits for a body long enough to make. - If devnull says no, why shows here and the draft stays. The Log's compose button opens the same composer for a new - commit, without the card. A draft stashed last time comes back in it, with the web's stash@{0} and drop on the left - of Commit's row. + The sheet under a commit (see OpensCommitSheet). Quote opens it as a composer: a bar of Cancel, the title and + Commit, which waits for a body long enough to make, over the field, focused, with the commit being quoted under it + as a static card, then the counter. On iOS Cancel is a Liquid Glass button and Commit a prominent one in your + colour; on Android they're text buttons, as in Material 3's full-screen dialog header. The title is centred. + Cancel closes it as a swipe does, keeping the draft. If devnull says no, why shows here and the draft stays. The + Log's compose button opens the same composer for a new commit, without the card. A draft stashed last time comes + back in it, with the web's stash@{0} and drop on the left of the counter's row. Report commit, in someone else's published commit's More menu, opens it as the web's Report dialog: the private, not-hidden line, the commit's body clamped to three lines, the four reasons, optional details, and Send report, @@ -11,7 +13,8 @@ here and the form keeps what you wrote. A new commit can become a `/feature` or `/bug` issue, as on the web (see DraftsIssues): the command's badge over the - field, its placeholder in it, and Open issue for Commit. Opened, the sheet says so, with View issue and Done. + field, its placeholder in it, and Open issue in Commit's place in the bar. Opened, the sheet says so, with View + issue and Done. It's full height for the forms, and short for the receipts. It's always on the screen, shown or not, so iOS can slide it away and say when it's gone. @@ -19,58 +22,62 @@ @if ($sheetCommit || in_array($sheetView, ['compose', 'issued'], true)) @if (in_array($sheetView, ['quote', 'compose'], true)) - @php($sheetLength = mb_strlen(trim($sheetBody))) @php($quoting = $sheetView === 'quote') @php($issueCommand = $sheetIssue === null ? null : \App\Devnull\IssueCommand::from($sheetIssue)) - - - {{ $quoting ? 'Quote '.$sheetCommit->author->handle() : 'New commit' }} - + + + Cancel + {{ $quoting ? 'Quote '.$sheetCommit->author->handle() : 'New commit' }} @if ($issueCommand) - @include('native.partials.issue-badge', ['command' => $issueCommand]) + {{-- The label stays put while it spins, so the button keeps its size. --}} + Open issue + @else + Commit @endif + + + + + @if ($sheetError) + {{ $sheetError }} + @endif - + @if ($issueCommand) + @include('native.partials.issue-badge', ['command' => $issueCommand]) + @endif - @if ($quoting) - @include('native.partials.quoted-commit', ['quoted' => $sheetCommit, 'static' => true]) - @elseif (! $issueCommand) - @include('native.partials.issue-suggestions', [ - 'suggestions' => $sheetTyped ? \App\Devnull\IssueCommand::suggested($sheetBody) : [], - 'hint' => $issueHintsDismissed ? null : \App\Devnull\IssueCommand::hinted($sheetBody), - ]) - @endif + - @if ($sheetError) - {{ $sheetError }} - @endif - - - @if ($sheetRestored) - @include('native.partials.stash-label', ['ref' => 'sheet-stash', 'drop' => 'dropSheetStash']) - + @if ($quoting) + @include('native.partials.quoted-commit', ['quoted' => $sheetCommit, 'static' => true]) + @elseif (! $issueCommand) + @include('native.partials.issue-suggestions', [ + 'suggestions' => $sheetTyped ? \App\Devnull\IssueCommand::suggested($sheetBody) : [], + 'hint' => $issueHintsDismissed ? null : \App\Devnull\IssueCommand::hinted($sheetBody), + ]) @endif - @include('native.partials.body-counter', ['body' => $sheetBody, 'ref' => 'sheet-budget']) - @if ($issueCommand) - {{-- The label stays put while it spins, so the button keeps its size. --}} - Open issue - @else - Commit - @endif - - - + + + @if ($sheetRestored) + @include('native.partials.stash-label', ['ref' => 'sheet-stash', 'drop' => 'dropSheetStash']) + + @endif + @include('native.partials.body-counter', ['body' => $sheetBody, 'ref' => 'sheet-budget']) + + + + @elseif ($sheetView === 'report') diff --git a/tests/Feature/Screens/ComposeSheetTest.php b/tests/Feature/Screens/ComposeSheetTest.php index f3f2aae..f5b3d66 100644 --- a/tests/Feature/Screens/ComposeSheetTest.php +++ b/tests/Feature/Screens/ComposeSheetTest.php @@ -138,6 +138,30 @@ function sheetCommitDisabled(TestableComponent $screen): bool Http::assertNotSent(fn (Request $request): bool => $request->method() === 'POST'); }); +it('closes the sheet from Cancel in its bar, keeping the draft as a swipe does', function () { + $screen = composeSheetOnLog()->input('sheet-body', 'Half a thought')->tap('sheet-cancel'); + + expect(nodeByRef($screen, 'sheet')['props']['visible'])->toBeFalse(); + + $screen->dismissSheet('sheet')->press('composeInSheet'); + + expect(nodeByRef($screen, 'sheet-body')['props']['value'])->toBe('Half a thought') + ->and(nodeByRef($screen, 'sheet-stash'))->not->toBeEmpty(); +}); + +it('draws the bar on Liquid Glass on iOS, and as a full-screen dialog header on Android', function () { + $commit = nodeByRef(composeSheetOnLog(), 'sheet-commit'); + + expect($commit['props']['glass'] ?? null)->toBe(3) + ->and($commit['props']['label'])->toBe('Commit'); + + fakeApi(['GET /api/v1/log/following' => [pageJson([])]]); + $android = Native::visit('/log', platform: 'android')->press('composeInSheet'); + + expect(nodeByRef($android, 'sheet-commit')['props'])->toMatchArray(['label' => 'Commit', 'variant' => 'ghost']) + ->and(nodeByRef($android, 'sheet-commit')['props'])->not->toHaveKey('glass'); +}); + it('only commits from a sheet that has a composer in it', function () { fakeApi(['GET /api/v1/log/following' => [pageJson([commitJson(['id' => 'c1', 'author' => userJson(['id' => 'u2', 'username' => 'ada'])])])]]);