Last Notes
Same batch, third part — and this one is blocked on an English decision, so do not land it yet.
@nprofile…jgnm's Italian for `appearance_palette_note_ink`, keeping the quotes as he asks:
> "Carta e penna" trasforma l'applicazione in un disegno su carta, e il colore che scegli è la nota di colore - le chat non lette, i messaggi che invii, ?????
The question marks are his, deliberately: he could not translate **"the one button to tap"** because he could not tell what it meant. So I went and read the colour roles, and the answer is that under Ink that button is **not** the chosen colour — `sendButtonColor` gives Ink the `primary`, and Ink's `primary` is the ink itself, measured equal to `onSurface` to the decimal in all six palettes and both modes. The English names something the look deliberately does not do, and has since the look shipped in v0.1.29-pre.1.
Filed as its own issue, since it is a defect in the English source and not an Italian problem: https://gitworkshop.dev/npub1sht0nuy83g6gu0dae0nkmh5n2r68kyvvat6r3hrdc0y2c5y8latsk2jm0v/cordn.feeds.relay.tools/staircase/issues/nevent1qywhwumn8ghj7cm0wfjxutnxv4jkguewwfjkccte9e6x7mmvwvqzq6dy9lurwmddul9ks4d3zpq83qcpezscp5llw2wqn26vth5qm80tewzec5 — it has the measurements and the role table.
What that means for this batch:
- **Land now**: `appearance_look_ink` → "Carta e penna", and his `appearance_palette_note_pro` line (both in the comments above). Neither depends on the English.
- **Hold**: `appearance_palette_note_ink`. Translating the third item before the English says something true just moves the error into Italian. Once the English clause is settled, @nprofile…jgnm finishes his sentence and it lands with it — he has already given the hard part, and note he drops the "light" metaphor here too ("la nota di colore") and renders the whole image as "un disegno su carta", a drawing on paper, rather than ink on paper.
One thing he did *not* ask for and I am not inferring: whether "i messaggi che invii" ("the messages you send") should stay as his rendering of "your own messages". It reads well and it is his call; just flagging that it is a slight reframing rather than a literal, in case someone diffs it and wonders.
Reported by @nprofile…jgnm.
Same batch, @nprofile…jgnm, 2026-10-30 — a second correction, and it is the bigger of the two: the **name of the look** is wrong.
> the correct translation for "ink & paper" is "carta e penna", in italian the literal translation "inchiostro e carta" sounds sketchy
So `appearance_look_ink` is **"Carta e penna"**, not "Inchiostro e carta". This matters more than one string because the name appears in the picker and is then quoted back inside the descriptions, and because it explains his `appearance_palette_note_pro` wording in the comment above: "Il tema «Staircase» è **un carta e penna** ma con la forma di un'app per telefono" is naming the other look, so the two strings only hang together once the name is fixed.
Three places carry it at origin/main (`it.xml`):
- `:107` `appearance_look_ink` — "Inchiostro e carta" → **"Carta e penna"**.
- `:124` `appearance_palette_note_ink` — opens with the name *and* repeats the image: "Inchiostro e carta disegna tutta l'app come inchiostro su carta". Both halves are the literalism he is flagging, so this sentence needs his wording, not a mechanical swap of the first two words. Asked him for it; will follow up here.
- `:125` `appearance_palette_note_pro` — already corrected in his line above, which uses the new name.
Worth a look while someone is in there: `Inchiostro`/`inchiostro` appears nowhere else in `it.xml`, so these three are the whole of it, and no other catalogue is affected — this is an Italian idiom call, not a problem with the English name.
One layout note for the pull request, since "Carta e penna" is a touch longer than "Inchiostro e carta" is not — it is shorter, so nothing crowds. The look picker row has room.
Reported by @nprofile…jgnm.
Correction batch from @nprofile…jgnm, 2026-10-30. One string, plus a wording problem in the English source behind it.
## The correction: `appearance_palette_note_pro`
He hit it while reading the Italian in the app and could not work out what it meant — then said he had not understood the English either. His wording, which wins:
> Il tema "Staircase" è un carta e penna ma con la forma di un'app per telefono: angoli omogenei, titoli decorati, elenchi raggruppati. Il colore che scegli è l'unica nota di colore - le chat non lette, i link e il pulsante di invio.
Against what is in `it.xml` today, the changes are: it names the theme ("Il tema «Staircase» è un carta e penna ma...") instead of opening with the bare product name; **"angoli omogenei"** for even corners, not "angoli regolari"; **"titoli decorati"** for serif titles, not "titoli con grazie"; **"l'unica nota di colore"** for the metaphor, not the literal "la luce al suo interno"; **"il pulsante di invio"**, not "il pulsante che invia".
Two notes for whoever lands it, neither a change to his wording:
- He typed **`é`** (acute) in both places where Italian wants **`è`** (grave) — a keyboard artefact, not a choice. The rest of `it.xml` is already `è`. Land `è`.
- He wrote a plain hyphen where the catalogue uses an em dash `—` as the clause break. Either is fine; the other eleven catalogues use the em dash, so keep it for consistency unless he says otherwise.
## The same metaphor is in the string next door
`appearance_palette_note_ink` carries the identical phrase — "the colour you pick is **the light in it**" — and the Italian has the identical literal rendering, "la luce al suo interno". It needs the same treatment in the same pull request, or the two looks will describe themselves in two different registers. Suggested, in his idiom and for him to confirm:
> Inchiostro e carta disegna tutta l'app come inchiostro su carta; il colore che scegli è l'unica nota di colore — le chat non lette, i tuoi messaggi, l'unico pulsante da toccare.
## The English behind it is worth plainer words
This one is not a translation bug. @nprofile…jgnm reads English comfortably and reported the **English** as not understandable: "I honestly didn't understand the English one either". Both strings say "the colour you pick is the light in it", meaning: these two looks draw everything in ink on paper, so the palette is the only colour in the app and is spent on the few things that should stand out. That is a nice image and it does not survive a reader who has not been told what it means — nor a translator, as the Italian shows, where word-for-word gives "light inside an object".
Not for me to rewrite @nprofile…juxr's prose, so this is a flag, not a patch: if the English becomes something like "the colour you pick is the only colour in it", the metaphor stops needing eleven separate rescues. If it stays as it is, that is a decision too — but then every catalogue needs the idiom, not the words, and the four already shipped (es, de, fr, plus it) should be checked for the same literalism.
Reported by @nprofile…jgnm.
@nprofile…juxr confirms this is the row he meant, and wants both halves: "if that's a multi select it should be more apparent and yeah fix up the jumping".
So neither half is optional here — a tab strip that reads as tabs rather than as pickable emoji, and a panel whose rows stay put when the category changes. Verify steps 2 and 3 are those two.
@nprofile…juxr, in the room: the worry is that rendering a note opens a bottomless pit of having to render things.
Writing the floor down here, because it is the decision this issue actually needs and it is one line of policy.
**A note card draws its content through `MessageBody` and composes nothing else.**
A bubble today is `MessageBody` *plus* four other things gathered beside it (`ChatScreen.kt:1866-1877`): invite
cards, gitworkshop cards, linked-image previews, and the row's own attachment. `MessageBody` on its own is the
bounded part — text, @mentions, URLs as links, custom emoji, the `> ` quote run — and it already terminates:
an event reference inside it draws as the one-line label, so a note quoting a note gives a label, not a second
card, with no special case and no depth counter. A card that calls only `MessageBody` inherits that floor for
free.
What that costs, and it is worth saying out loud rather than discovering later: a note whose body is an image URL
shows the URL, not the picture; a note quoting another note shows "📝 note by @alice"; a note with an attachment
shows nothing of it. That is a card that is honest about being a preview, and every one of those can be lifted
later by gathering the same lists the bubble gathers — the point is that none of them has to be lifted now.
The genuinely new machinery is not the rendering at all, it is **fetch-an-event-by-id**, and we have one already:
`GitWorkshopCards` (`app-core/.../GitWorkshop.kt:131`) fetches from a reference's relay hints, once per
reference, cached, in-flight shared, 8 s timeout, miss retried after 60 s, with "Reading from …" and "Could not
read it" states. Keyed on an `EventRef` instead of a gitworkshop link, that is the whole of it.
So the scope, smallest honest version: a fetcher keyed on `EventRef`, a card composable that draws author +
kind + time + `MessageBody`, `nevent`/`note` to a kind-1 note only, everything else left as the label it is
today. The rest of this issue's body is the version with the lid off; it does not have to ship together.
Refinement from @nprofile…juxr, same room, minutes after the one above: **"open in" an installed nostr app on Android**
— Amethyst or whatever the person has — rather than always being thrown at njump in a browser.
This one is worth having whether or not the card lands, because today it is the *only* thing a tap does:
`eventRefUrl` (`MessageText.kt:46`) hard-codes `https://njump.me/<bech32>` and `MessageBody` hands that to
`host.openUrl`.
What is already there:
- `MainActivity.kt:97` is `startActivity(Intent(ACTION_VIEW, Uri.parse(url)))`, so handing it `nostr:<bech32>`
dispatches to any installed client that claims the `nostr` scheme. Nothing new is needed on the Android side
except deciding to pass that string.
- Our manifest claims `nostrsigner` and `cordn`, not `nostr`, so Staircase cannot catch its own intent — no loop,
no chooser with us in it.
- The catch: that line is `runCatching { … }`, so with no nostr app installed the tap silently does nothing.
**The fallback to a web viewer has to be explicit and has to live in the host**, which is the only layer that
can tell whether the intent resolved; common code cannot. Desktop can try the same URI through
`ExternalOpener` and fall back; the browser cannot resolve a scheme at all, so there njump stays as it is.
cordn-web's ⋯ menu is three web viewers (nostr.at, njump.me, jumble.social) — a browser client has no other
option — so this is one place not to copy it wholesale: on Android the first item should be the installed app,
with the web viewers under it.
Extra Verify step, after step 2:
2a. On a phone with a nostr client installed (Amethyst), tap the reference: it opens there, not in the browser.
Uninstall it (or try on a phone with none) and tap again: it falls back to the web viewer rather than doing
nothing. On desktop and in the browser the behaviour is unchanged.
@nprofile…juxr asked for the same thing today, in the bug-report room: render notes in place rather than sending a tap
to njump. Recording him as a reporter here rather than filing a second issue.
One narrowing from him, worth taking as the first cut: **`nevent1…` probably only**. The body above already splits
that way — prose kinds (1, and 30023 by `naddr`) render their content, every other kind stays a label-only card —
so a first pull request that draws a card for a `nevent`/`note` reference to a kind-1 note and leaves `naddr` and
the rest exactly as they are today satisfies both asks and keeps Verify 5 for later. Steps 1-4 and 6-9 stand as
written.
Also since this was filed: the one-line label it replaces is currently invisible inside your own bubble under the
Staircase look (#9b0fbb80, the link colour is the bubble's own colour). Unrelated cause, but it is the same text,
so whoever takes this should know the "before" they are comparing against is worse than the issue describes.
Did not reproduce in the cohort run on raven against v0.1.29-pre.19 (main f2b70328, debug APK md5 `cf94d05a12553fb1fcf99f3da096509e`), on the new Settings > Appearance > Emoji packs search — which goes through the same `RelaySearch` sockets.
The first search of the session ("cats", app launched ~90 s earlier, account "Android testoor" signing with Amber) returned 11 hits in under 3 s with no error line on screen and no Amber prompt at all. Logcat for that tap:
```
newlay.relay wss://search.brainstorm.world connected
newlay.relay ... CLOSED sub-1: auth-required: this relay answers through a web of trust and has no house observer to lend you. Sign in (NIP-42 ...), or name whose trust ranks this read with the NIP-50 `observer:<64-hex pubkey>` token, or ask for the whole corpus unranked with `include:spam`.
newlay.relay ... NOTICE: restricted: no kind 10040 for you here — ranked search will be empty
```
So the relay refused the ranked ask, the app fell back to the observer token inside the same tap, and the answer came back — the signer was never involved, which is presumably why there was no window to race. Two further searches in the session behaved the same. Not evidence the defect is gone; evidence that this path can take the auth-required refusal without asking the signer, which may be worth comparing against the people search's route when the fix is written.
A second, cleaner reproduction of this from the same run, on the **web** rather than a phone, with a starting point anyone can repeat in a couple of minutes. It makes the "nothing mints a replacement" half of the title concrete.
1. Sign in to the web app with a key, in a browser profile. It publishes one last-resort key package — here `11244952` for f1832fac.
2. Get invited into a group and accept. Works.
3. Destroy that browser profile. The install, and the private half of `11244952` with it, is gone. Nothing on the coordinator changes.
4. Sign in with the same key in a fresh browser profile.
The new install **publishes no key package of its own** — `cordn_key_packages` still returns exactly `11244952 · 2026-10-29 · last-resort`, same ref, nothing added. That is the mint gate doing what it is meant to do: another install already serves one for this account, so this one does not mint. But no install holds that one any more.
From there the account is permanently uninvitable and nothing says so. Inviting it from Staircase's Add people succeeds from the inviter's side and the group shows the new member in good standing; on the new install the Inbox reads **"Keys that belong to none of your devices"** and **"1 invitation this device could not open"**. I repeated the invite into a second, brand-new group with the same result, so it is not one unlucky Welcome.
The contrast that isolates it: a key with no prior key package at all, onboarded on the same web build in the same way minutes later, published its own (`76338337`), was invited into a new group, accepted, and read the chat normally. So the only difference is whether a stale package was already being served.
Which puts the gate in an awkward spot — it is right that a second live install should not mint, and wrong that an install replacing a dead one does not. Whatever decides "another install already serves one" cannot currently tell a sibling from a ghost. The phone case I commented on earlier is the same shape with the extra wrinkle that the stale entries were months of accumulated ordinary ones rather than a single last-resort.
Worth adding that the repair I found on the phone — another member's "Let back in", off a join request — is not reachable here either, because the new install is already a member from everyone else's point of view and Add people answers "Already in this group" with Invite disabled.
Builds: web `:app-web:webDistribution` of main e71ba963 (v0.1.29-pre.13), headless Chromium 153, coordinator cordn.feeds.relay.tools. From cohort run test-sprig, requested by @nprofile…juxr.
Hit this on raven during cohort run test-sprig, on main e71ba963 (v0.1.29-pre.13), with the debug build installed fresh (md5 1a5e5eb1) over the existing one — so the app's data, and any private halves it held, were preserved.
The account is the phone's own (56cbd175). `cordn_key_packages` on this coordinator listed seven: five ordinary from 2026-10-24 and 10-26, two last-resort from 10-18, with the tool's own warning that "every one of them is stale, so none may still be held by the client they read in". cordn-web, inviting from its member picker, chose the last-resort `7b0980e2` and sealed the Welcome to it. The phone could not open it: the Inbox showed **"1 invitation this device could not open"**, while every other member's view of the group listed the account as a member in good standing. So the uninvitable state is not visible from the inviting side at all — the group just quietly contains someone who cannot read it.
Two things worth adding to the record here.
**Nothing minted a replacement, including the path that says it will.** The New chat › "Open a link or code" screen, given the group's link, offers "Request to join" with the line *"Your key packages are published on its coordinator too, so anyone there can add you."* Tapping it published nothing: `cordn_key_packages` returned the identical seven entries, same refs, same dates, before and after. If that wording is a promise, it is not kept; if it describes only what happens on approval, it reads as though it happens on request.
**There is a working repair, and it is not the checkup.** The request did reach the group as a join request, on another member's Inbox, worded "npub12m9a…pwu7 is in Sprig already and asking to be let back in — their copy of the chat stopped working. Approving replaces their place in the group, so they can read it again; nobody else is affected." One tap on "Let back in" and the phone could read the chat; the timeline showed "npub12m9a…pwu7 was re-added". That is a clean route out of exactly the state this issue describes, and it needs another member rather than anything the stranded device can do alone.
The account checkup was the other route offered ("Check my account · 2 things") but its first question is whether the stray keys belong to another device of the owner's, and answering it sweeps or takes over that account's keys — not something to walk on someone else's phone in a test room, so I left it. Worth knowing that the two repairs have very different blast radii.
Reported from the cohort run @nprofile…juxr asked for; happy to re-walk any part of it on a phone if this needs more.