diff --git a/app/Devnull/Data/Message.php b/app/Devnull/Data/Message.php index 3bdb418..bed3442 100644 --- a/app/Devnull/Data/Message.php +++ b/app/Devnull/Data/Message.php @@ -295,6 +295,26 @@ public function snippet(): string return self::snippetOf($this->body, count($this->images), $this->voice !== null); } + /** + * The links in its body, in the order it draws them, which its rows open by their place among them. + * + * @return list + */ + public function links(): array + { + $links = []; + + foreach ($this->content as $block) { + foreach ($block->runs as $run) { + if ($run->link !== null) { + $links[] = $run->link; + } + } + } + + return $links; + } + private static function snippetOf(string $body, int $images, bool $hasVoice = false): string { if ($hasVoice) { diff --git a/app/NativeComponents/Conversation.php b/app/NativeComponents/Conversation.php index 47d4125..a91099b 100644 --- a/app/NativeComponents/Conversation.php +++ b/app/NativeComponents/Conversation.php @@ -1182,6 +1182,23 @@ public function openLink(int $id): void } } + /** + * Opens a link in a message's body, by its place among the body's links: a + * commit or another page on dev/null the app has a screen for here in the + * app, anything else in the browser. + */ + public function openMessageLink(int $id, int $index): void + { + $link = $this->findMessage($id)?->links()[$index] ?? null; + $screen = $link?->commit !== null ? '/commits/'.$link->commit : SiteLink::screen($link?->href); + + if ($screen !== null) { + $this->openInTab($screen); + } elseif (SiteLink::isWebUrl($link?->href)) { + Browser::inApp((string) $link?->href); + } + } + /** * Pins the message at once, first in the list as the latest pinned, as the web does. */ diff --git a/resources/views/native/partials/message-body.blade.php b/resources/views/native/partials/message-body.blade.php index 7dbe77e..9edf3c3 100644 --- a/resources/views/native/partials/message-body.blade.php +++ b/resources/views/native/partials/message-body.blade.php @@ -1,24 +1,36 @@ {{-- A message's body, drawn from its segments as a commit's is (see partials/commit-body): prose with tappable - mentions and inline code, hex colors beside their swatch, quotes behind a rule on the left, and fenced code as - mono text in a box. $ink is the bubble's text colour, so a body on your lane reads in that lane's label colour, - and $quoteRule the quote's rule: amber, or the label colour on your lane, as the web's pale rule in your own - bubbles. + mentions, links and inline code, hex colors beside their swatch, quotes behind a rule on the left, and fenced + code as mono text in a box. $ink is the bubble's text colour, so a body on your lane reads in that lane's label + colour, and $quoteRule the quote's rule: amber, or the label colour on your lane, as the web's pale rule in your + own bubbles. + A link is in $linkInk, Commit Blue off your lane and the lane's label colour on it, with a named link's owner + muted in $linkPrefixInk, and opens by its place among the body's links (see Conversation::openMessageLink). + $maxLines caps the body to the first block at that many lines, for the pinned sheet's preview, so its links work + before Show more. --}} -@foreach ($message->content as $block) +@php($linkInk ??= $mentionInk) +@php($linkPrefixInk ??= 'text-theme-ink-muted') +@php($maxLines ??= null) +@php($linkIndex = 0) +@foreach ($maxLines === null ? $message->content : array_slice($message->content, 0, 1) as $block) @if ($block->isCode()) - + @else @if ($block->isQuote()) @endif -@foreach ($block->runs as $run) +@foreach ($block->runs as $run) @if ($run->isMention()) @elseif ($run->isCode) +@elseif ($run->link?->isNamed()) +⁠ +@elseif ($run->link) + @elseif ($run->isColor())   @else diff --git a/resources/views/native/partials/message-item.blade.php b/resources/views/native/partials/message-item.blade.php index 54c75a6..88b7ae6 100644 --- a/resources/views/native/partials/message-item.blade.php +++ b/resources/views/native/partials/message-item.blade.php @@ -75,13 +75,13 @@ @if ($message->isPending()) - @include('native.partials.message-body', ['ink' => $message->status === 'rejected' ? 'text-theme-ink' : 'text-theme-on-'.$myColour, 'mentionInk' => 'text-theme-on-'.$myColour, 'quoteRule' => $message->status === 'rejected' ? 'border-theme-lane-5' : 'border-theme-on-'.$myColour]) + @include('native.partials.message-body', ['ink' => $message->status === 'rejected' ? 'text-theme-ink' : 'text-theme-on-'.$myColour, 'mentionInk' => 'text-theme-on-'.$myColour, 'quoteRule' => $message->status === 'rejected' ? 'border-theme-lane-5' : 'border-theme-on-'.$myColour, 'linkPrefixInk' => $message->status === 'rejected' ? 'text-theme-ink-muted' : 'text-theme-on-'.$myColour.'/70']) @else - @include('native.partials.message-body', ['ink' => $mine ? 'text-theme-on-'.$myColour : 'text-theme-ink', 'mentionInk' => $mine ? 'text-theme-on-'.$myColour : 'text-theme-primary', 'quoteRule' => $mine ? 'border-theme-on-'.$myColour : 'border-theme-lane-5']) + @include('native.partials.message-body', ['ink' => $mine ? 'text-theme-on-'.$myColour : 'text-theme-ink', 'mentionInk' => $mine ? 'text-theme-on-'.$myColour : 'text-theme-primary', 'quoteRule' => $mine ? 'border-theme-on-'.$myColour : 'border-theme-lane-5', 'linkPrefixInk' => $mine ? 'text-theme-on-'.$myColour.'/70' : 'text-theme-ink-muted']) @endif diff --git a/resources/views/native/partials/pinned-message.blade.php b/resources/views/native/partials/pinned-message.blade.php index cdf8219..3cfb3a7 100644 --- a/resources/views/native/partials/pinned-message.blade.php +++ b/resources/views/native/partials/pinned-message.blade.php @@ -50,7 +50,9 @@ @if ($message->body !== '') @if ($isLong && ! $isExpanded) - + + @include('native.partials.message-body', ['ink' => 'text-theme-ink', 'mentionInk' => 'text-theme-primary', 'quoteRule' => 'border-theme-lane-5', 'maxLines' => \App\NativeComponents\Conversation::PinnedLines]) + @else @include('native.partials.message-body', ['ink' => 'text-theme-ink', 'mentionInk' => 'text-theme-primary', 'quoteRule' => 'border-theme-lane-5']) diff --git a/tests/Feature/Screens/ConversationTest.php b/tests/Feature/Screens/ConversationTest.php index eea047f..a850fd0 100644 --- a/tests/Feature/Screens/ConversationTest.php +++ b/tests/Feature/Screens/ConversationTest.php @@ -733,6 +733,32 @@ function pulledAvatars(TestableComponent $screen, int $id): array ->assertNativeCalledTimes('Browser.OpenInApp', 1); }); +it('opens a link in a message body, in the app for a page on dev/null and in the browser for any other', function () { + $commit = '0199e5c4-0000-7000-8000-0000000000c1'; + + serveRemoteThread(threadJson([ + remoteMessageJson(['id' => 1, 'body' => "read https://nativephp.com/docs or https://dev-null.sh/@grace then https://dev-null.sh/@ada/commits/{$commit}"]), + ], ['read_through_id' => 1])); + + $screen = finishRefreshes(Native::visit('/remotes/conversations/ada')); + + expect(paragraphRuns($screen, 'read'))->toBe([ + ['read '], + ['nativephp.com/docs', 'link'], + [' or '], + ['dev-null.sh/@grace', 'link'], + [' then '], + ['ada/00000c1', 'link'], + ]); + + $screen->press('openMessageLink(1, 0)') + ->assertNativeCalled('Browser.OpenInApp', fn (array $params): bool => $params['url'] === 'https://nativephp.com/docs') + ->assertNoNavigation(); + + finishRefreshes(Native::visit('/remotes/conversations/ada'))->press('openMessageLink(1, 1)')->assertNavigatedTo('/remotes/users/grace'); + finishRefreshes(Native::visit('/remotes/conversations/ada'))->press('openMessageLink(1, 2)')->assertNavigatedTo('/remotes/commits/'.$commit); +}); + it("opens a mentioned organization's page, and a mentioned user's profile, in the tab", function () { serveRemoteThread(threadJson([ remoteMessageJson(['id' => 1, 'body' => 'ask @laravel or @grace', 'mentions' => [ @@ -1148,6 +1174,31 @@ function serveOrgThread(array $thread, ?array $poll = null, ?Closure $pins = nul expect(findNode(nodeByRef($screen, 'pin-1'), fn (array $node): bool => ($node['props']['max_lines'] ?? null) === 3))->toBeNull(); }); +it('opens a link in a pinned message, in the sheet and in its three-line preview', function () { + $commit = '0199e5c4-0000-7000-8000-0000000000c1'; + $long = "release checklist: https://nativephp.com/docs\n1. tag 2.4\n2. post the changelog\n3. ping the mobile folks"; + + serveOrgThread(orgThreadJson([], ['pinned' => [ + orgMessageJson(['id' => 1, 'body' => $long]), + orgMessageJson(['id' => 2, 'body' => 'fixed in https://dev-null.sh/@ada/commits/'.$commit]), + ]])); + + $screen = finishRefreshes(Native::visit('/remotes/orgs/org-1'))->tap('pinned-bar'); + + $preview = findNode(nodeByRef($screen, 'pin-1'), fn (array $node): bool => ($node['props']['max_lines'] ?? null) === 3); + $link = $preview === null ? null : findNode($preview, fn (array $node): bool => ($node['props']['text'] ?? null) === 'nativephp.com/docs'); + + expect($preview)->not->toBeNull() + ->and($link['on_press'] ?? null)->not->toBeNull(); + + $screen->press('openMessageLink(1, 0)') + ->assertNativeCalled('Browser.OpenInApp', fn (array $params): bool => $params['url'] === 'https://nativephp.com/docs'); + + finishRefreshes(Native::visit('/remotes/orgs/org-1'))->tap('pinned-bar') + ->press('openMessageLink(2, 0)') + ->assertNavigatedTo('/remotes/commits/'.$commit); +}); + it('opens Reply, React and Copy text on a long press on a message, yours or theirs, and keeps its tap and double tap', function () { serveRemoteThread(threadJson([ remoteMessageJson(['id' => 1, 'body' => 'did you see the PR?']),