diff --git a/app/NativeComponents/Conversation.php b/app/NativeComponents/Conversation.php index 2302f41..5dbf439 100644 --- a/app/NativeComponents/Conversation.php +++ b/app/NativeComponents/Conversation.php @@ -132,8 +132,8 @@ class Conversation extends NativeComponent public const array QuickReactions = ['😂', '✅', '❤️']; /** - * How far a message is pulled toward its edge before letting go replies to it. Reply comes in over that distance from - * the other side of the thread. + * How far a message is pulled toward its edge before letting go replies to it. Reply grows in beside the bubble over + * that distance. */ public const int ReplyPull = 64; diff --git a/composer.json b/composer.json index bfde957..122d0d5 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,10 @@ "type": "vcs", "url": "https://github.com/simonhamp/toastkit" }, + { + "type": "vcs", + "url": "https://github.com/NativePHP/mobile-ui" + }, { "name": "nativephp-plugins", "type": "composer", @@ -33,7 +37,7 @@ "laravel/boost": "^2.0", "laravel/framework": "^13.0", "laravel/tinker": "^3.0", - "nativephp/mobile": "^4.7", + "nativephp/mobile": "dev-main as 4.8.0", "nativephp/mobile-browser": "^1.2", "nativephp/mobile-camera": "^1.1", "nativephp/mobile-clipboard": "^1.0", @@ -43,7 +47,7 @@ "nativephp/mobile-secure-storage": "^1.1", "nativephp/mobile-share": "^1.1", "nativephp/mobile-skia": "^1.0", - "nativephp/mobile-ui": "^0.9", + "nativephp/mobile-ui": "dev-main", "s2br/nativephp-mobile-splashscreen": "^1.4", "victorycodedev/toastkit": "dev-fix/android-toasts-above-sheets as 1.4.0" }, diff --git a/composer.lock b/composer.lock index e9cd601..eefe798 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "745d59f03866816f94bab0b20f027a25", + "content-hash": "4564a3ffac02898824b683cfa1aedddd", "packages": [ { "name": "bacon/bacon-qr-code", @@ -1534,16 +1534,16 @@ }, { "name": "laravel/framework", - "version": "v13.39.0", + "version": "v13.40.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "d52c3b3129e4c8e213c97cdc68f94b0b7d27f181" + "reference": "1449bf241017087fbd38c118ecaabef3f6797fdc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/d52c3b3129e4c8e213c97cdc68f94b0b7d27f181", - "reference": "d52c3b3129e4c8e213c97cdc68f94b0b7d27f181", + "url": "https://api.github.com/repos/laravel/framework/zipball/1449bf241017087fbd38c118ecaabef3f6797fdc", + "reference": "1449bf241017087fbd38c118ecaabef3f6797fdc", "shasum": "" }, "require": { @@ -1761,7 +1761,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-10-27T14:04:39+00:00" + "time": "2026-11-03T15:07:18+00:00" }, { "name": "laravel/mcp", @@ -2090,16 +2090,16 @@ }, { "name": "league/commonmark", - "version": "2.10.4", + "version": "2.10.7", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "40821b7606d8d946bac57dbd2290076c722e3a65" + "reference": "8eb53e906fd60c67038d22f1f1cab420ed58b8be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/40821b7606d8d946bac57dbd2290076c722e3a65", - "reference": "40821b7606d8d946bac57dbd2290076c722e3a65", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/8eb53e906fd60c67038d22f1f1cab420ed58b8be", + "reference": "8eb53e906fd60c67038d22f1f1cab420ed58b8be", "shasum": "" }, "require": { @@ -2193,7 +2193,7 @@ "type": "tidelift" } ], - "time": "2026-10-17T19:59:50+00:00" + "time": "2026-10-31T13:35:37+00:00" }, { "name": "league/config", @@ -2755,16 +2755,16 @@ }, { "name": "nativephp/mobile", - "version": "4.7.0", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/NativePHP/mobile-air.git", - "reference": "0d7e2e6509b1a0514726e67016fecade5e9137b2" + "reference": "c0da5d781df99416fb92f398a354ceac0b532922" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/NativePHP/mobile-air/zipball/0d7e2e6509b1a0514726e67016fecade5e9137b2", - "reference": "0d7e2e6509b1a0514726e67016fecade5e9137b2", + "url": "https://api.github.com/repos/NativePHP/mobile-air/zipball/c0da5d781df99416fb92f398a354ceac0b532922", + "reference": "c0da5d781df99416fb92f398a354ceac0b532922", "shasum": "" }, "require": { @@ -2804,6 +2804,7 @@ "suggest": { "ext-zip": "Required by native:install to validate and extract the downloaded Android PHP binary archives" }, + "default-branch": true, "type": "library", "extra": { "laravel": { @@ -2846,9 +2847,9 @@ ], "support": { "issues": "https://github.com/NativePHP/mobile-air/issues", - "source": "https://github.com/NativePHP/mobile-air/tree/4.7.0" + "source": "https://github.com/NativePHP/mobile-air/tree/main" }, - "time": "2026-10-29T17:31:02+00:00" + "time": "2026-11-05T15:42:02+00:00" }, { "name": "nativephp/mobile-browser", @@ -3355,16 +3356,16 @@ }, { "name": "nativephp/mobile-ui", - "version": "0.9.0", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/NativePHP/mobile-ui.git", - "reference": "9cee87cbc676a6500247bec99be5d19f633a04e9" + "reference": "ce627333f74293a719e1b86891cec09772f74b4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/NativePHP/mobile-ui/zipball/9cee87cbc676a6500247bec99be5d19f633a04e9", - "reference": "9cee87cbc676a6500247bec99be5d19f633a04e9", + "url": "https://api.github.com/repos/NativePHP/mobile-ui/zipball/ce627333f74293a719e1b86891cec09772f74b4a", + "reference": "ce627333f74293a719e1b86891cec09772f74b4a", "shasum": "" }, "require": { @@ -3376,6 +3377,7 @@ "orchestra/testbench": "^10.0", "pestphp/pest": "^3.0" }, + "default-branch": true, "type": "nativephp-plugin", "extra": { "laravel": { @@ -3392,7 +3394,17 @@ "Native\\Mobile\\UI\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "scripts": { + "test": [ + "pest" + ], + "lint": [ + "pint" + ], + "lint:test": [ + "pint --test" + ] + }, "license": [ "MIT" ], @@ -3404,16 +3416,16 @@ ], "description": "Native SwiftUI and Jetpack Composer components for NativePHP", "support": { - "issues": "https://github.com/NativePHP/mobile-ui/issues", - "source": "https://github.com/NativePHP/mobile-ui/tree/0.9.0" + "source": "https://github.com/NativePHP/mobile-ui/tree/main", + "issues": "https://github.com/NativePHP/mobile-ui/issues" }, "funding": [ { - "url": "https://nativephp.com/sponsor", - "type": "other" + "type": "other", + "url": "https://nativephp.com/sponsor" } ], - "time": "2026-10-29T17:44:09+00:00" + "time": "2026-11-05T16:47:04+00:00" }, { "name": "nesbot/carbon", @@ -11562,6 +11574,12 @@ } ], "aliases": [ + { + "package": "nativephp/mobile", + "version": "dev-main", + "alias": "4.8.0", + "alias_normalized": "4.8.0.0" + }, { "package": "victorycodedev/toastkit", "version": "dev-fix/android-toasts-above-sheets", @@ -11571,6 +11589,8 @@ ], "minimum-stability": "stable", "stability-flags": { + "nativephp/mobile": 20, + "nativephp/mobile-ui": 20, "victorycodedev/toastkit": 20 }, "prefer-stable": true, diff --git a/resources/views/native/partials/message-item.blade.php b/resources/views/native/partials/message-item.blade.php index 77b67da..1944691 100644 --- a/resources/views/native/partials/message-item.blade.php +++ b/resources/views/native/partials/message-item.blade.php @@ -6,8 +6,8 @@ message the web gives a status, in that status. Pulling it toward its edge replies to it: someone else's to the left and your own to the right. It follows the - finger that way only, partly off the screen, though a short bubble stops while some of it still shows. Reply slides - in from the other side of the thread as it grows, turns upright and fades in. At Conversation::ReplyPull the phone + finger that way only, partly off the screen, though a short bubble stops while some of it still shows. Reply grows + in a little way off the bubble's other side (see partials/message-swipe-reply). At Conversation::ReplyPull the phone buzzes lightly; let go there and the composer replies to it and takes the keyboard. It springs back either way. The pull, the buzz and the spring back all run on the phone, and only the reply comes here. A double tap toggles a heart. A pinned message keeps its pin beside it. A message still being pushed can't be pulled, and nothing replies @@ -49,18 +49,22 @@ @endif {{-- - An image-only message keeps its pin beside its images, and React's quick reactions point at them; otherwise - those go with the bubble. The bubble's row stops short of the far side, so a long message doesn't reach it. + An image-only message keeps its pin and Reply beside its images, and React's quick reactions point at them; + otherwise those go with the bubble. The bubble's row stops short of the far side, so a long message doesn't + reach it. --}} @if ($message->images !== [] && $message->body === '') - @if ($mine) - @include('native.partials.message-pin') - @endif - @include('native.partials.message-images', ['photosMenu' => $menu]) - @if (! $mine) - @include('native.partials.message-pin') - @endif + + @if ($mine) + @include('native.partials.message-pin') + @endif + @include('native.partials.message-images', ['photosMenu' => $menu]) + @if (! $mine) + @include('native.partials.message-pin') + @endif + @include('native.partials.message-swipe-reply') + @if ($reactionsFor === $message->id && ! $reactsFromPinned) @include('native.partials.quick-reactions', ['anchor' => $reactionsAnchor]) @endif @@ -72,27 +76,30 @@ @include('native.partials.message-voice') @if ($message->body !== '') - {{-- The bubble shrinks to wrap short of the far side, and of its pin when it has one. --}} + {{-- The bubble shrinks to wrap short of the far side, and of its pin when it has one. Reply sits just past both. --}} - @if ($mine) - @include('native.partials.message-pin') - @endif - @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, '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', 'linkPrefixInk' => $mine ? 'text-theme-on-'.$myColour.'/70' : 'text-theme-ink-muted']) - - - @endif - @if (! $mine) - @include('native.partials.message-pin') - @endif + + @if ($mine) + @include('native.partials.message-pin') + @endif + @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, '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', 'linkPrefixInk' => $mine ? 'text-theme-on-'.$myColour.'/70' : 'text-theme-ink-muted']) + + + @endif + @if (! $mine) + @include('native.partials.message-pin') + @endif + @include('native.partials.message-swipe-reply') + @if ($reactionsFor === $message->id && ! $reactsFromPinned) @include('native.partials.quick-reactions', ['anchor' => $reactionsAnchor]) @endif @@ -131,20 +138,5 @@ @if ($replyable) - {{-- - Reply, just past the far side of the thread, comes in with the finger as the message goes the other way: - growing from 40%, turning upright from a quarter turn and fading in, and in place once the pull reaches - ReplyPull. It's off the row, so it never lies over a bubble and follows the finger even where a short - bubble has stopped. - --}} - - - - - @endif diff --git a/resources/views/native/partials/message-swipe-reply.blade.php b/resources/views/native/partials/message-swipe-reply.blade.php new file mode 100644 index 0000000..f9ecba9 --- /dev/null +++ b/resources/views/native/partials/message-swipe-reply.blade.php @@ -0,0 +1,17 @@ +{{-- + Reply, a little way past the bubble on the side a pull uncovers (past its pin when it has one), so it's where you're + looking. It's laid over the bubble's row as the row is tall, so it's centred on the bubble, and it goes with it. It + grows from 40%, turns upright from a quarter turn and fades in as the pull goes, and is in place at + Conversation::ReplyPull. Lying over the row rather than in it, it takes none of the bubble's room on either + platform, so the bubble wraps where it would without it. +--}} +@if ($replyable) + + + + + +@endif diff --git a/resources/views/native/partials/message-voice.blade.php b/resources/views/native/partials/message-voice.blade.php index a17f4d9..abb7b57 100644 --- a/resources/views/native/partials/message-voice.blade.php +++ b/resources/views/native/partials/message-voice.blade.php @@ -1,6 +1,6 @@ {{-- - A message's voice note, in a bubble of its own as the web draws it, with the message's pin beside it as a text - bubble has it (see partials/message-item). Yours is in your lane, staged in it faded until devnull has it, or + A message's voice note, in a bubble of its own as the web draws it, with the message's pin and Reply beside it as a + text bubble has them (see partials/message-item). Yours is in your lane, staged in it faded until devnull has it, or outlined in red once it turned the note down; everyone else's is on the wash. The player is partials/voice-note. A long press opens the message's menu, as on a text bubble, without Copy text, and React's quick reactions point at the bubble. @@ -8,23 +8,26 @@ @if ($message->voice !== null) @php($voiceInk = $message->status === 'rejected' ? 'ink' : ($mine ? 'on-'.$myColour : 'ink')) - @if ($mine) - @include('native.partials.message-pin') - @endif - @if ($message->isPending()) - - @include('native.partials.voice-note', ['voice' => $message->voice, 'voiceKey' => $message->voiceKey(), 'ref' => 'voice-'.$key, 'ink' => $voiceInk]) - - @else - - + + @if ($mine) + @include('native.partials.message-pin') + @endif + @if ($message->isPending()) + @include('native.partials.voice-note', ['voice' => $message->voice, 'voiceKey' => $message->voiceKey(), 'ref' => 'voice-'.$key, 'ink' => $voiceInk]) - - @endif - @if (! $mine) - @include('native.partials.message-pin') - @endif + @else + + + @include('native.partials.voice-note', ['voice' => $message->voice, 'voiceKey' => $message->voiceKey(), 'ref' => 'voice-'.$key, 'ink' => $voiceInk]) + + + @endif + @if (! $mine) + @include('native.partials.message-pin') + @endif + @include('native.partials.message-swipe-reply') + @if ($reactionsFor === $message->id && ! $reactsFromPinned) @include('native.partials.quick-reactions', ['anchor' => $reactionsAnchor]) @endif diff --git a/tests/Feature/Screens/ConversationTest.php b/tests/Feature/Screens/ConversationTest.php index 14d6bc7..2e4f23d 100644 --- a/tests/Feature/Screens/ConversationTest.php +++ b/tests/Feature/Screens/ConversationTest.php @@ -63,9 +63,9 @@ function serveRemoteThread(array $thread, ?array $poll = null, int $pushStatus = // Theirs at the start of the row and yours at the end, each on its own, in a thread that opens on its latest message. expect(nodeByRef($screen, 'content-1')['layout']['justify_content'])->toBe(0) - ->and(childRefs($screen, 'content-1'))->toBe(['bubble-1']) + ->and(childRefs($screen, 'bubble-and-pin-1'))->toBe(['bubble-1', 'swipe-reply-1']) ->and(nodeByRef($screen, 'content-2')['layout']['justify_content'])->toBe(2) - ->and(childRefs($screen, 'content-2'))->toBe(['bubble-2']) + ->and(childRefs($screen, 'bubble-and-pin-2'))->toBe(['bubble-2', 'swipe-reply-2']) ->and(nodeByRef($screen, 'thread')['props']['scroll_anchor'] ?? null)->toBe('bottom'); }); @@ -161,6 +161,11 @@ function serveRemoteThread(array $thread, ?array $poll = null, int $pushStatus = ->assertSee('staged') ->assertSet('draft', ''); + // Until devnull has it, it can't be pulled, so it has no Reply. + $staged = $screen->instance()->pending[0]->clientId; + + expect(childRefs($screen, "bubble-and-pin-{$staged}"))->toBe(["bubble-{$staged}"]); + finishRefreshes($screen)->assertSee('pushed')->assertSet('pending', []); $sent = json_decode(sentTo('POST', 'remotes/@ada/messages')[0], true); @@ -204,7 +209,7 @@ function serveRemoteThread(array $thread, ?array $poll = null, int $pushStatus = expect(array_map(fn (string $body): string => json_decode($body, true)['client_id'], sentTo('POST', 'remotes/@ada/messages')))->toBe([$clientId, $clientId]); }); -it('pulls theirs left and yours right, toward their edges, with Reply coming in from the other side', function () { +it('pulls theirs left and yours right, toward their edges, with Reply coming in just off the bubble', function () { serveRemoteThread(threadJson([ remoteMessageJson(['id' => 1, 'body' => 'did you see the PR?']), remoteMessageJson(['id' => 2, 'user_id' => RemotesViewerId, 'body' => 'which one?']), @@ -222,18 +227,22 @@ function serveRemoteThread(array $thread, ?array $poll = null, int $pushStatus = ->and(nodeByRef($screen, 'pulled-1')['props']['translate-x_sv'])->toBe($pan('1')) ->and(nodeByRef($screen, 'pulled-2')['props']['translate-x_sv'])->toBe($pan('2')); - // Reply waits just past the far side of the row, off the bubbles, and follows the finger in, growing, turning - // upright and fading in, all the way by ReplyPull. Insets go top, right, bottom, left. - expect(nodeByRef($screen, 'swipe-reply-1')['layout'])->toMatchArray(['width' => $pull, 'position' => [-0.0, -$pull, -0.0, 0.0]]) - ->and(nodeByRef($screen, 'swipe-reply-2')['layout']['position'])->toEqual([-0.0, 0.0, -0.0, -$pull]) + // Reply lies over the bubble's own row, as tall as it so it's centred on the bubble, 8 points past the side the pull + // uncovers, and goes with it, growing, turning upright and fading in, all the way by ReplyPull. Lying over the row + // and not in it, it takes none of the bubble's room. Insets go top, right, bottom, left. + expect(childRefs($screen, 'content-1'))->toBe(['bubble-and-pin-1']) + ->and(childRefs($screen, 'bubble-and-pin-1'))->toBe(['bubble-1', 'swipe-reply-1']) + ->and(childRefs($screen, 'bubble-and-pin-2'))->toBe(['bubble-2', 'swipe-reply-2']) + ->and(nodeByRef($screen, 'content-1')['layout']['padding'])->toEqual([0, 64, 0, 0]) + ->and(nodeByRef($screen, 'swipe-reply-1')['layout'])->toMatchArray(['width' => 30.0, 'position' => [-0.0, -38.0, -0.0, 0.0]]) + ->and(nodeByRef($screen, 'swipe-reply-2')['layout']['position'])->toEqual([-0.0, 0.0, -0.0, -38.0]) + ->and(nodeByRef($screen, 'swipe-reply-1')['props'])->not->toHaveKey('translate-x_sv') ->and(nodeByRef($screen, 'swipe-reply-1')['props'])->toMatchArray([ - 'translate-x_sv' => $pan('1'), 'opacity_sv' => $pan('1')."|interp:-{$pull},0:1,0", 'scale_sv' => $pan('1')."|interp:-{$pull},0:1,0.4", 'rotate_sv' => $pan('1')."|interp:-{$pull},0:0,90", ]) ->and(nodeByRef($screen, 'swipe-reply-2')['props'])->toMatchArray([ - 'translate-x_sv' => $pan('2'), 'opacity_sv' => $pan('2')."|interp:0,{$pull}:0,1", 'rotate_sv' => $pan('2')."|interp:0,{$pull}:-90,0", ]); @@ -293,7 +302,7 @@ function serveRemoteThread(array $thread, ?array $poll = null, int $pushStatus = expect($bubble['type'])->toBe('popover') ->and($bubble['props']['visible'])->toBeTrue() - ->and(childRefs($screen, 'content-1'))->toBe(['bubble-1', 'reactions-bubble']) + ->and(childRefs($screen, 'content-1'))->toBe(['bubble-and-pin-1', 'reactions-bubble']) ->and(childRefs($screen, 'quick-reactions'))->toBe(['quick-0', 'quick-1', 'quick-2', 'more-reactions']); // A tap outside it closes it. @@ -303,7 +312,7 @@ function serveRemoteThread(array $thread, ?array $poll = null, int $pushStatus = $screen->tap('react-menu-2'); - expect(childRefs($screen, 'content-2'))->toBe(['bubble-2', 'reactions-bubble']); + expect(childRefs($screen, 'content-2'))->toBe(['bubble-and-pin-2', 'reactions-bubble']); }); it("points the quick reactions at an image-only message's images, and pulls it to reply like any other", function () { @@ -311,7 +320,8 @@ function serveRemoteThread(array $thread, ?array $poll = null, int $pushStatus = $screen = finishRefreshes(Native::visit('/remotes/conversations/ada'))->tap('react-menu-1'); - expect(childRefs($screen, 'content-1'))->toBe(['photos-1', 'reactions-bubble']); + expect(childRefs($screen, 'content-1'))->toBe(['bubble-and-pin-1', 'reactions-bubble']) + ->and(childRefs($screen, 'bubble-and-pin-1'))->toBe(['photos-1', 'swipe-reply-1']); $screen->trigger('swipe-1')->assertSet('replyToId', 1); }); @@ -850,7 +860,7 @@ function serveOrgThread(array $thread, ?array $poll = null, ?Closure $pins = nul ->emitNative(ButtonPressed::class, ['index' => 1, 'label' => Conversation::PinMessage, 'id' => Conversation::PinAlert]) ->assertSee('pinned · 1'); - expect(childRefs($screen, 'content-1'))->toBe(['bubble-1', 'pinned-1']) + expect(childRefs($screen, 'bubble-and-pin-1'))->toBe(['bubble-1', 'pinned-1', 'swipe-reply-1']) ->and(array_column(menuItems($screen, 'bubble-1'), 0))->toContain('unpin-menu-1'); finishRefreshes($screen)->assertSee('pinned · 2'); @@ -881,8 +891,8 @@ function serveOrgThread(array $thread, ?array $poll = null, ?Closure $pins = nul $screen = finishRefreshes(Native::visit('/remotes/orgs/org-1')); - expect(childRefs($screen, 'content-1'))->toBe(['bubble-1', 'pinned-1']) - ->and(childRefs($screen, 'content-2'))->toBe(['pinned-2', 'bubble-2']) + expect(childRefs($screen, 'bubble-and-pin-1'))->toBe(['bubble-1', 'pinned-1', 'swipe-reply-1']) + ->and(childRefs($screen, 'bubble-and-pin-2'))->toBe(['pinned-2', 'bubble-2', 'swipe-reply-2']) ->and(nodeByRef($screen, 'pinned-1')['type'])->toBe('stack'); });