Make the phone work: touch reordering, reachable menus, and nothing lost offline
Four things that were built for a mouse on a desk and quietly did nothing useful on a phone. The last one is the serious one. **A note made offline lost everything written into it.** Its create reached the server, the server named it something else, and every edit still queued behind it went on addressing the name only that device ever knew — 404, five retries, thrown away. The note arrived empty and untitled, with nothing to say that anything had been lost. The database was repointed at the new id all along; the queue was not. It is now, by replacing the id across the whole queued payload rather than in a list of named fields, because a list of fields is a list to forget to add to. The same held for a notebook made offline and everything filed into it. **The order of notes could not be changed on a phone**, because the list went through the HTML5 drag API, which mobile browsers never fire from touch. Pointer events instead, which mouse, pen and finger all produce. A mouse still drags a row from anywhere; a finger drags from a grip, because pressing the row has to go on scrolling and holding it has to go on opening the menu. The row under the finger is found with elementFromPoint — a finger raises no pointerover on anything it did not press. **"Move to..." opened off the side of the screen**, and could not be opened at all without a hover. It is a tap now, unfolding in place where there is no room beside the menu and flipping sides where there is. The menu itself is placed by measuring rather than by assuming it is 220px wide, its rows are tall enough to hit with a thumb, and Escape closes it. **Installing said nothing when it could not be done.** The entry appeared only when the browser had offered a prompt, so where installing was impossible there was no button and no reason. It is always there now and names the cause — most usefully that a browser will only install a site served over HTTPS, which is also why the service worker would not have registered, so one cause was showing up as two mysteries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6eecf57247
commit
608da16ac2
12 files changed
+743
-147
No files matched your search
@@ -314,6 +314,45 @@ bug they cover actually happened:
|
||||
- `hasPendingWrites` must cover the debounce window, not just requests in flight
|
||||
- v1 note bodies must migrate without losing task state or smuggling in markup
|
||||
|
||||
### Reordering, and menus, with a finger
|
||||
|
||||
Both were built for a mouse and quietly did nothing on a phone.
|
||||
|
||||
**Dragging** went through the HTML5 drag-and-drop API, which mobile browsers
|
||||
never fire from touch — so the arrangement could not be changed at all, with
|
||||
nothing on screen to say why. [`useReorderDrag`](frontend/src/hooks/useReorderDrag.ts)
|
||||
uses pointer events instead, which mouse, pen and finger all produce. The two
|
||||
devices get different starts, because they want different things:
|
||||
|
||||
- **A mouse** begins a drag by pressing anywhere on a row and moving a few pixels.
|
||||
- **A finger** begins one only from the grip. Pressing the row has to go on
|
||||
scrolling the list, and holding it has to go on opening the menu; a drag that
|
||||
started from anywhere would take both.
|
||||
|
||||
The row under the finger is found with `elementFromPoint`, not with enter/leave
|
||||
handlers — a finger generates no `pointerover` on anything it did not press.
|
||||
|
||||
**The context menu** was placed with `innerWidth - 220`, a stand-in for a width
|
||||
nobody had measured, and its submenu was pinned to `left: 100%` and opened only
|
||||
on hover. On a phone the parent already sits against the right edge, so the list
|
||||
of notebooks under *Move to…* appeared past the edge of the display — and with no
|
||||
hover, there was no way to open it in the first place. It is now placed by
|
||||
measuring, opens on a tap, unfolds in place on a narrow screen, and flips to the
|
||||
other side on a wide one when the right would not fit.
|
||||
|
||||
### Installing it
|
||||
|
||||
The install entry used to appear only when the browser had offered a prompt
|
||||
(`beforeinstallprompt`). Where installing was impossible there was nothing on
|
||||
screen at all — no button, no reason, and no way to find out short of knowing how
|
||||
PWAs work.
|
||||
|
||||
It is always there now, and when there is no prompt it says which of the fixable
|
||||
causes applies. The common one is worth naming: **a browser will only install a
|
||||
site served over HTTPS**, and on plain HTTP the service worker never registers
|
||||
either — so one cause takes the install *and* offline with it, which otherwise
|
||||
looks like two unrelated mysteries.
|
||||
|
||||
### Notebook covers
|
||||
|
||||
A cover is stored as an **index** into `COVERS` in
|
||||
@@ -350,7 +389,7 @@ docker compose -f docker-compose.dev.yml up -d --build # must be running
|
||||
| Suite | Checks | What it holds down |
|
||||
| --- | --- | --- |
|
||||
| `admin` | 15 | no sign-up anywhere; `/api/admin/*` refused for non-admins even when called directly |
|
||||
| `editor` | 31 | tables and their right-click menu, task lists, note links, backlinks — and that each survives a reload |
|
||||
| `editor` | 36 | tables and their right-click menu, task lists, note links, backlinks — and that each survives a reload |
|
||||
| `search-attachments` | 12 | body-only matches, ranking, umlauts; images as blob URLs, not inline bytes |
|
||||
| `history` | 10 | a large deletion is snapshotted, restoring works, restoring is itself undoable |
|
||||
| `drawing` | 18 | strokes survive the poller, match across four viewport widths, and stay on the page they were drawn on |
|
||||
@@ -358,8 +397,8 @@ docker compose -f docker-compose.dev.yml up -d --build # must be running
|
||||
| `attachments-lifecycle` | 10 | a picture is kept while any version shows it, and freed once none does |
|
||||
| `conflict` | 10 | a late write wins but the state it buries survives as a version |
|
||||
| `export` | 21 | the downloaded bytes are real Markdown, ZIP and PNG, unpacked and read back |
|
||||
| `organise` | 39 | a hand-made order survives a search; separators hold their place; a note exported and imported comes back whole |
|
||||
| `offline` | 12 | a cold page load with the network off still opens notes and searches them |
|
||||
| `organise` | 50 | a hand-made order survives a search; separators hold their place; a note exported and imported comes back whole |
|
||||
| `offline` | 17 | a cold page load with the network off still opens notes and searches them — and a note made, written and drawn on offline arrives whole once there is a connection |
|
||||
| `sync` | 7 | a PDF written on one device — document, drawing and pinned notes — reaches a second one, which can find it by its contents without opening it |
|
||||
|
||||
Each suite creates a throwaway account through the admin API and deletes it
|
||||
@@ -1128,6 +1167,30 @@ out as a ZIP rather than as its first page alone.
|
||||
|
||||
---
|
||||
|
||||
## Made offline, and what it is called
|
||||
|
||||
Something created with no connection carries an id this device invented —
|
||||
`local_x9k2…`. When its create finally reaches the server, the server names it
|
||||
something else, and the local database is repointed at the new name.
|
||||
|
||||
**The queue has to be repointed too**, and for a long time it was not. The create
|
||||
arrived; every edit still queued behind it went on addressing the name only this
|
||||
device ever knew; the server answered 404; the entry was retried five times and
|
||||
thrown away. A note written and drawn on offline reached the server *empty and
|
||||
untitled*, with nothing to say that anything had been lost. The same held for a
|
||||
notebook made offline and everything filed into it.
|
||||
|
||||
`repointQueue(oldId, newId)` in [`offlineStore.ts`](frontend/src/offlineStore.ts)
|
||||
now rewrites the waiting entries alongside the database. It replaces the id as a
|
||||
plain string across the whole queued payload rather than in a list of named
|
||||
fields — ids are long random strings, so a false match is not a practical worry,
|
||||
and a list of fields is a list to forget to add to.
|
||||
|
||||
The `offline` suite makes a note with the network off, titles it, writes in it,
|
||||
draws on it, reconnects, and then asks the *server* for all three.
|
||||
|
||||
---
|
||||
|
||||
## Conflict Resolution
|
||||
|
||||
Two devices can edit the same note without knowing about each other — a phone
|
||||
|
||||
Reference in new issue
Block a user