Skip to content

Bring Remotes to the app - #25

Merged
simonhamp merged 5 commits into
mainfrom
feat/remotes
Oct 19, 2026
Merged

simonhamp merged 5 commits into
mainfrom
feat/remotes

Conversation

@simonhamp

@simonhamp simonhamp commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Brings the web's Remotes (direct messages) to the app. Remotes takes Saved's place in the tab bar, and Saved opens from your own profile.

What's in it

Remotes tab. The web's inbox: Open, Stashed, and Fetched while devnull takes requests. They're pages you swipe between, like the Log and Notifications. The pager's tabs are text only, so Fetched's request count goes in its label ("Fetched · 2"). Each row shows the partner's avatar ringed in their lane (or two of an org's people), the name and handle (or "org · N"), and the last message in the web's words ("you: …", "Ada: …" in an org, "sent an image", or the org event). On the right it shows "N ahead" in your lane, or the relative time. Your own last message wears the web's pushed or pulled glyph. Pull to refresh and paging work as on the web. The tab badge is the unread count the web's nav shows.

New remote and New org. The web's two buttons sit at the top right of the inbox, with its icons (users-round and square-pen). New remote finds a developer by name or handle and opens your conversation with them in the picker's place. New org takes an optional name and people, up to the web's 100 with you, shows them as chips you tap to take back out, and opens the new org. Someone you can't reach shows the web's reason ("doesn't follow you", "has remotes off", "the org is full (100)") and can't be picked. A term nobody matches gets the web's fatal: nothing matches '…'.

Thread. Opens on the latest message, at the bottom:

  • Yours on the right in your lane, theirs on the left.
  • Day markers, HEAD · N new where you left off, org events as log lines, and the author over a run of someone else's messages in an org.
  • Reply ghosts, images, link cards and reaction chips.
  • The web's status under the right message: staged, pushed, "pulled 14:05", or "pulled by …" in an org.
  • A tap on a message shows React and Reply right beside its bubble, with the web's icons, as the web shows them on hover. They fade in and slide a few points into place. The bubble's row always keeps their slot, so a long message wraps short of it and nothing moves. React opens a sheet with the web's quick reactions (😂 ✅ ❤️, swapped for ones you use more) and more emoji. A double tap toggles a heart. I dropped the long press I had first because it was unreliable.
  • Swiping a message sideways replies to it, as the web does on touch screens: someone else's to the right, your own to the left. The field takes focus. Other swipes, like a flick while scrolling, do nothing.
  • The nav bar menu has View profile and Stash or Pop, with the web's archive icons.

Composer. In the bottom bar, with the same room above and below the field so it sits evenly on the keyboard. "@ada is typing…" shows under the title in the nav bar rather than above the composer as on the web. A line in the bar hid messages before they reached its border.

  • Push shows the message staged at once, then pushed. If devnull refuses it, it shows rejected with retry, which resends under the same client id.
  • Typing pings go out at most every 3 seconds, as on the web.
  • The counter appears from 1,600 of 2,000.
  • When the composer can't push, the bar says why in the web's words. A request gets Drop and Merge.

Account. The web's Remotes switch, word for word, under Pastéis mode. Under it, Share pulls and, only while devnull offers it, who can push to you. Each saves optimistically and goes back with the web's error if devnull says no.

Profile. Someone else's profile gets the web's Remote button beside Follow when you could push to them: they follow you, or devnull takes requests. Your own profile gets a Saved button in the nav bar beside Account.

Remotes off. The tab shows the web's 404 for /remotes, word for word, with "Back to your log", since that's what the web shows.

How new messages arrive

Polling, like the web. There are no websockets yet.

  • The inbox polls every 15 seconds while it's showing.
  • A thread polls for messages after the last one, plus reactions changed since the version it saw. It polls every 2 seconds within a minute of anyone writing or typing, and every 4 seconds otherwise. The web does 1.5 and 4; #[Poll] ticks every 2 seconds and an idle thread skips every other tick.
  • Polls go out on the background lane (RefreshesInBackground) and are never cached. Only a thread's first page and the inbox are kept, so opening one is instant.
  • New messages are marked pulled as they arrive, since the thread is on screen.

The thread is a scroll-anchor="bottom" scroll view with each message as its own row. This PR bumps nativephp/mobile-ui to combined/dev-null-app b70ef42 for NativePHP/mobile-ui#141. With that, a message arriving follows you if you're at the bottom and leaves you alone if you're reading back. Before, it yanked you down. The view also adds the home indicator at its end while the keyboard is down, so the thread pads its end for the composer bar alone and sits right above it with the keyboard up or down. Nothing else in the lock moved.

API it needs

Each part has its own upstream PR. Until one lands, that part of the app just doesn't answer.

A devnull without #649 sends no remotes on /me, and the app takes that as on, so the tab doesn't show the 404 just because the API is older.

Not in this PR

  • Sending images. Signed requests can't carry files until the commit-images API change adds multipart signing. Received images show.
  • Org management. Adding and removing people, renaming and leaving stay on the web. Creating one is in.
  • Swiping inbox rows to stash. I tried it, but row swipes fought the swipes between tabs, so it's out. Stash and Pop stay in each thread's menu.
  • Deleting or editing messages. The web has neither.
  • Saved at the top left of your profile. The nav bar can't hold a leading button yet. Adding one is a core change, and mobile-air's combined branch currently also carries the 4.1.0 PHP binaries pin from Install the PHP binaries that grow the frame buffers #18, so it waits for Install the PHP binaries that grow the frame buffers #18. For now Saved sits beside Account.

Checked on a simulator

On an iPhone 17 Pro simulator (iOS 27), I ran this against a local devnull with all six API PRs merged. Two test users talked to each other: local_dev in the app, and dking on the web in a browser.

  • Inbox with "2 ahead" and the tab badge.
  • Thread with a code span, a link card, and HEAD.
  • local_dev pushes and sees staged then pushed. The web shows it, and dking's messages show pulled on the web.
  • dking types on the web and the app shows "@dking is typing…".
  • dking's message arrives by polling within about a second, and the app pulls it.
  • Reactions both ways: 🚀 from the app shows on the web, and ❤️ from the web shows in the app.
  • A reply with its ghost.
  • An org dking created on the web shows up through the 15-second inbox poll, with its event line, author header and "pulled by".
  • An image dking sent loads through the signed URL.
  • Remotes off shows the web's 404, and turning it back on brings the conversations back.
  • Saved opens from the profile, and Remote on dking's profile opens the thread.
  • With messages arriving: reading a few rows up, the view stays put. At the bottom, it follows.
  • Dark mode.

The simulator reached the local API at 127.0.0.1. A cloudflared tunnel was refused by this session's permission settings, so no tunnel was involved. UIMenu items (the thread's ⋯ menu) didn't take AXe taps, so View profile and Stash are covered by feature tests only.

Inbox Thread Typing Reactions
Org Remotes off Account Remote on a profile

Following at the bottom, then staying put while reading back:

The screenshots are on the orphan branch remotes-screenshots, to delete after merge.

Since then, against the same local API with #734 added, I checked on the simulator:

  • the two buttons;
  • New remote's hint and its no-match line;
  • New org's layout;
  • swiping between Open and Stashed;
  • the tighter Push box.

The same build is on Simon's iPhone against that API over the LAN. AXe's typing didn't reach the simulator this time, so search results, picking and swipe to reply are covered by feature tests only so far.

Known rough edges

  • Message times are in UTC. That's the app's timezone. The scheduling PR's Timezone reads the phone's, and once it lands, Display::clock() should use it the way the day labels will.
  • Don't merge before the API. Until #654 is on dev-null.sh, the inbox gets a 404 and the tab would show the "Remotes off" page.

Tests

Feature tests for the inbox and its pager, the thread, swipe to reply, New remote and New org, the Account section, the Profile button and the cache. All 920 pass, and Pint is clean.

🤖 Generated with Claude Code

simonhamp and others added 5 commits October 19, 2026 19:42
Remotes takes Saved's place in the tab bar, and Saved opens from your own
profile. The inbox and threads poll as the web's do, messages push staged
and then pushed, and the Account screen gains the web's Remotes switch
and Share pulls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… turned up

Each message is now a row of the thread's scroll view, not of one column
inside it, so SwiftUI keeps a reader's place by the rows on screen. With
NativePHP/mobile-ui#141 (combined/dev-null-app 6e92378), a message
arriving no longer pulls someone reading back down to it, and still
follows someone at the bottom.

Bubbles get a set width, since a column that only hugged its bubble
measured the text on one line and clipped it. HEAD only shows when
something above it was read, as on the web. The thread's own path()
clashed with OpensInTab's and silently stopped View profile and mention
taps. And a devnull too old to send remotes on /me no longer reads as
Remotes turned off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ipe to reply

Remotes gets the web's two buttons at the top right, with its icons: New org
(users-round) and New remote (square-pen). New remote finds a developer by
name or handle and opens your conversation with them. New org takes an
optional name and people, up to the web's 100 with you, and opens the new
org. Someone you can't reach shows the web's reason and can't be picked.
Both use the people search and org creation in nunomaduro/dev-null.sh#734.

Open, Fetched and Stashed are now pages you swipe between, like the Log and
Notifications. The pager's tabs are text only, so Fetched's request count
goes in its label.

Swiping a message sideways replies to it, as the web does on touch screens:
someone else's to the right, your own to the left. The field takes focus.
Any other swipe, such as a flick while scrolling, does nothing.

The typing line now sits above the composer's border, as on the web, so the
Push box is tighter. Stash and Pop in a thread's menu use the web's archive
icons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A long press on a message was unreliable, so it's gone. A tap now shows
React and Reply beside the message, with the web's icons (smile-plus and
reply), as the web shows them on hover. Reply sits nearest the bubble. The
row keeps a gutter for them on every message, so they come and go without
moving anything. React opens the reactions sheet, which no longer carries
Reply. A double tap still toggles a heart, and a swipe still replies.

Who's typing moves under the title in the nav bar. The empty typing line
above the composer was part of the bar, so it hid messages before they
reached the bar's border.

The field has the same room above and below, so the bar sits evenly on
the keyboard. The thread now pads its end for the bar alone. mobile-ui's
bottom anchor adds the home indicator while the keyboard is down
(NativePHP/mobile-ui#141, combined/dev-null-app b70ef42). With the keyboard
up there's no longer a big gap above the bar, and a short thread no longer
floats up the screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The buttons sat in a gutter at the edge of the screen, away from short
messages. Now the bubble and a slot for them share a row, so they sit
right against the bubble, after someone else's and before yours. The slot
keeps its width, so a long message wraps short of it either way, and
nothing moves when they appear. An image-only message keeps them beside
its images.

Every message keeps its buttons, faded out and tucked 8 points toward the
bubble. A tap fades them in and slides them into place over 180ms, and
fades out the last message's. Faded out, they take no taps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@simonhamp
simonhamp marked this pull request as ready for review October 19, 2026 23:36
@simonhamp
simonhamp merged commit b71930a into main Oct 19, 2026
1 check passed
@simonhamp
simonhamp deleted the feat/remotes branch October 19, 2026 23:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant