Closes the Android half of the 0.32 unification, and moves CI to Gitea Actions in the same change.
What it does
Merges release/android-0-32 into main. 26 conflict hunks across 15 files, 160 files changed. Replaces Gitea PR #186, which cannot merge.
Fixes two defects the first pipeline found: a stray } in client/Conversations.kt and five vector paths the release branch's CI predated.
Moves CI from .gitlab-ci.yml to .gitea/workflows/ci.yml, with the union of both sides' vector sets and the :games module added.
The resolutions worth a human read
Conversations.kt: git paired the release branch's new uploadAttachment doc and parameters with main's sendGroupAttachment body. Either side taken whole would have deleted a function, and taking the release side would also have dropped attachment padding, a privacy property under SPEC 23.7 and 23.9. Both functions are written out whole; the padding survives.
MainActivity.kt: both openLinkIfAsked and openHelloLinkIfAsked are kept, hello first, because helloLinkIn filters by HelloLink.isAppScheme and leaves the intent alone while the contact handler consumes any VIEW dataString.
Session.kt and MessengerService.kt: main's arrived filter fix is kept, and quiet threads are subtracted before the notification decision (val loud = touched - s.quietThreads).
Strings
879 keys in English and 879 in Spanish, no key missing from either.
Pin
.e2e-ref is 4b4042f72880758ee2dea7713f7a2cb63fa12f73, which is now Gitea flamenet-e2e main. All 15 vector files exist at that commit.
Not fixed here
DirectoryScreen.kt, GameUi.kt and EventUi.kt still hold inline English from the release branch. That predates this merge and is out of its scope.
A green pipeline proves the tree compiles and the suites pass. It says nothing about whether the notification and padding merges behave as intended, so those deserve a read.
Closes the Android half of the 0.32 unification, and moves CI to Gitea Actions in the same change.
**What it does**
1. Merges `release/android-0-32` into `main`. 26 conflict hunks across 15 files, 160 files changed. Replaces Gitea PR #186, which cannot merge.
2. Fixes two defects the first pipeline found: a stray `}` in `client/Conversations.kt` and five vector paths the release branch's CI predated.
3. Moves CI from `.gitlab-ci.yml` to `.gitea/workflows/ci.yml`, with the union of both sides' vector sets and the `:games` module added.
**The resolutions worth a human read**
- `Conversations.kt`: git paired the release branch's new `uploadAttachment` doc and parameters with main's `sendGroupAttachment` body. Either side taken whole would have deleted a function, and taking the release side would also have dropped attachment padding, a privacy property under SPEC 23.7 and 23.9. Both functions are written out whole; the padding survives.
- `MainActivity.kt`: both `openLinkIfAsked` and `openHelloLinkIfAsked` are kept, hello first, because `helloLinkIn` filters by `HelloLink.isAppScheme` and leaves the intent alone while the contact handler consumes any VIEW `dataString`.
- `Session.kt` and `MessengerService.kt`: main's `arrived` filter fix is kept, and quiet threads are subtracted before the notification decision (`val loud = touched - s.quietThreads`).
**Strings**
879 keys in English and 879 in Spanish, no key missing from either.
**Pin**
`.e2e-ref` is `4b4042f72880758ee2dea7713f7a2cb63fa12f73`, which is now Gitea `flamenet-e2e` main. All 15 vector files exist at that commit.
**Not fixed here**
`DirectoryScreen.kt`, `GameUi.kt` and `EventUi.kt` still hold inline English from the release branch. That predates this merge and is out of its scope.
A green pipeline proves the tree compiles and the suites pass. It says nothing about whether the notification and padding merges behave as intended, so those deserve a read.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
DESIGN 1b and 3 add three kinds: card and event are renderable and carry
mid like a text; rsvp is control traffic and never a bubble, with or
without its ctl. Each is built by hand with its keys in the order both
apps agreed, and strings go through a new WireJson.quote that writes
what Swift's JSONEncoder writes with slashes left alone. The framework
org.json escapes every slash and the JVM copy does not, so the existing
quote could not give one set of bytes.
Decoding follows the iPhone: a card object is read leniently, an event
needs an id, a rev of at least 1, a title and a time, and anything
unusable falls through to the unsupported placeholder. Flags are read as
numbers or booleans, whole numbers may carry a fraction, and render
bounds and the sealed-until rule (capped at 400 days from arrival) live
on the types so they can be tested here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The highest rev from the organizer replaces the whole event and anything
else is ignored. The organizer is whoever sent rev 1; until rev 1 is
seen the first sender stands in, and a rev 1 from somebody else takes
over with its own content. Each member's answer with the highest count
wins. State maps onto a history row as ev, org and rsvp, the shape the
backup carries, plus a provisional mark that stays local.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A card travels as its wire object and an event as its latest accepted ev
with org and rsvp beside it, as agreed with the iOS build. Row keys left
out of the shared shape are now listed in BackupPayload.NOT_CARRIED with
a reason: the provisional organizer mark, and rxp, which was already
being dropped without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The iPhone reads an rsvp only when n is at least 1, so Android now does
too and both drop the same envelopes. The reason given for leaving rxp
out of the backup now says what the iPhone does with private reactions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 1a: every manifest under assets/stickers is a pack, the glyph set
first and then flamenet.retro, and the picker shows them as sections. A
sticker draws as its picture, then its glyph, then its pack's fallback,
and a pack this build lacks draws as the core fallback. A manifest's
image may only name a file in its own folder. Sending passes the pack
that was picked instead of always the core one.
The 24 retro PNGs and their manifest are the ART agent's output, byte
for byte the files in the iOS worktree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Session.deleteMessage went straight to the vault. For an off-the-record
thread the line is held in memory, so the delete found nothing, left the
line on screen, and rewrote the thread's history record, empty, to disk:
a record naming the person for a conversation §6.28 says is never
written down. It now edits the thread where it lives, through
mutateHistory, like edits and reactions. Cards and events go through the
same delete, which is how this turned up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 1b and 3 in the app, in one-to-one chats and groups.
Cards: the attach panel offers a card with one of the nine flamenet.io
fronts (shipped as assets/cards, titles drawn as text), to, message,
from, and an optional day it stays sealed until, at most 400 days ahead.
The receiver sees the front and "Opens on <date>" until then, and the
card opens by itself when the day comes; the sender's copy is open and
says whom it is sealed for. Copy, report and forward never hand over a
card still sealed here, and a forward goes out unsealed.
Events: title, a time or all day, an optional end, place and note. The
card shows when it is in the reader's zone, the organizer's time when
their clock reads differently, who answered what, Going / Maybe / Can't
go in one row with "Everyone in this chat sees your answer.", and Add to
calendar through the system's own insert sheet. Only the organizer is
offered Change event and Cancel event. Revisions go out under a fresh
mid and fold into the card the event first arrived on.
Arrivals: answers fold onto their event and only redraw it, never
marking a chat unread or notifying; one that comes before its event is
held in memory until the event lands. A card or event from this
account's own other device is kept only when it names a group. With
previews on, a card or an event that arrives alone is announced as
DESIGN words it, and never with a card's words.
Nothing new is written for an off-the-record chat: every change goes
through record and mutateHistory. Rows carry card, ev, org and rsvp, and
HistoryRowsCarryEveryFieldTest reads record() and Session.Line from
source and fails until a new field is carried by the backup, listed in
BackupPayload.NOT_CARRIED, and read back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two tests carried a narrow no-break space, a no-break space and a line
separator as literal characters, which read as ordinary spaces. They and
the emoji in the new tests and the sticker fallback are now \u escapes,
as the existing tests write them. The strings are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plain JVM module set up like :engine: chess, checkers, reversi and
tic-tac-toe as DESIGN.md section 2 describes them, with the same API as
the iOS FlamenetGames package. Moves are strings in the wire notation and
apply accepts exactly the strings legalMoves returns. Replay refuses a log
over the game's cap before playing it and names the index of the first
move it cannot play.
The tests run games-v1.json, byte for byte the file the iOS package runs:
the perft counts from DESIGN.md and 147 cases. Deeper published perft
counts run with FLAMENET_GAMES_DEEP=1.
Nothing in :app uses it yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN items 7 to 13, as both apps now do:
- An answer's n is the unix second it was given, or one past this
device's last answer if that was later. Receivers keep the highest n,
and on an equal n the answer that sorts higher wins (yes over no over
maybe), so one person answering on two devices in one second leaves
every phone with the same answer. Held answers use the same rule.
- A cancel notifies "<title> was cancelled" and the card reads
"Cancelled: <title>".
- An event id that is only spaces is malformed, as on the iPhone.
Comments and test names cite §6.31 (cards), §6.32 (events and answers),
§17.2 and docs/STICKERS.md instead of DESIGN.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in 597dd804: the chess, checkers, reversi and tic-tac-toe rules
engines the chat games are built on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`:games:test` joins the unit job's task list, the results gate counts
the module with a floor of 13 of its 15 tests, and its build directory
is kept out of Spotlight like the other modules'.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Content reads and writes `game`, `gmove` and `gend` exactly as §6.33
shows them: `mine` is the side the sender of that envelope plays, `log`
the whole game so far, `r` one of the six endings. A start is drawn as a
board in a one-to-one chat only; a game in a group, or one this build
cannot read, shows the unsupported placeholder. A move or an ending is
control traffic and never a bubble, with or without `ctl`.
A backup carries the game on its start message as
{"id","t","me","log","r","rby"} (DESIGN items 12 and 14). The out-of-step
flag and an open draw offer stay on the phone, each listed with its
reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A game lives on its start message's history row as the log alone, so it
follows that row through off the record (memory only), delete, the
disappearing timer and the backup. Everything else is replayed from the
log with the capped engine replay.
Receiving follows §6.33 rules 1 to 4: an unknown game is rebuilt from
the envelope; a stale or duplicate log changes nothing; a log that
disagrees, a peer adding a ply for this person's side, an illegal or
malformed move, or a log over the cap marks the game out of step, which
is sticky. Endings follow DESIGN items 16, 22 and 23: an ending is judged
at the log its gend carries, once that log is in step; a draw offer
counts only from the side that has just moved, at the receiver's whole
log, and the next move lapses it; decline and cancel count only until
the invitee's first move in that log; a decline for a game this phone
never saw rebuilds it as the game it started, declined.
Moves and endings go out announced, so the other phone wakes (item 15),
and are never queued: one that reaches nobody, a contact with no device
included, is taken back (item 24). The start is queued like a card.
Their move notifies without a sound, "Your move: chess with Sam"; an
ending says "<Game> with <name>: <result>"; a draw offer "<name> offers
a draw: <Game>"; a refused offer says nothing; a snoozed chat holds them
all. A game from another server, or from this account's own other
device, is never a board.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Game" sits in the attach row of a one-to-one chat only: pick chess,
checkers, reversi or tic-tac-toe, and who goes first. The card shows the
board from the engine's own board, row 0 at the top for the first player
and turned round for the second, with whose move it is.
Tap a piece and its targets are marked; tap one to send the move. A
checkers jump is entered square by square, the path marked as it goes,
and sent as one move once complete; a chess promotion asks for the
piece; reversi marks its legal squares and offers a lone Pass when that
is the only move; tic-tac-toe takes the tapped cell. Whatever is sent is
exactly a move the engine offered.
Under the board: decline or cancel before play starts, resign after a
confirmation, offer a draw once it is the other person's move, accept or
decline theirs, in one row; the result in words once it is over; "Your
boards disagree" with "Start a new game" (same game, same sides) when
out of step; and the moves, folded away. Plain colours that read in both
themes, and every square labelled with its name and what stands on it.
A move or an ending that did not reach them says so and changes nothing.
The attach row now scrolls sideways instead of squeezing five buttons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A one-to-one thread went into the shared backup under this app's own
name for it, "user:7", and came back under whatever key the file had.
The iPhone keys these threads "7" and skips any key that is not a
number, so every one-to-one conversation was lost when a backup moved
between the two phones, in either direction: the iPhone dropped
Android's threads, and Android filed the iPhone's under "7", where
nothing reads them.
The prefix now comes off on the way out and goes back on for a numeric
key on the way in. A file written before this, keyed "user:7", still
restores into the same thread. The test goes through the vault's own
export and import, where the payload-only checks passed all along; the
older tests that used a bare id as this app's thread name now use the
real one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scanning a code on Home filled the add field with the literal text
${card.fnid}@${card.relay}: the line in 130edd7 was written with ${'$'}
escapes, so the template was never evaluated. Every scan on Home gave
the same unusable string.
homePrefill() turns a scanned code into the address, bound by the fnid
as the audit asked, and Home uses it. The test pins both the address
and the absence of the escaped template in MainActivity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TodayFile checks a Today envelope (SPEC 8.11) before reading a field
of it: size, version, a pinned kid, the Ed25519 signature over the body
bytes, then a body with a real date and a made time. Lists are cut to
their bounds, strings are cut on character boundaries, and a link that
is not http or https drops its entry.
TodayFeed asks the home relay for /today.json through the default
client, so a bridge carries it: no Authorization, cookie, id or query,
and If-None-Match only when it is the hash of the copy held (DESIGN
19). At most once an hour from the relay's last answer, five minutes
after a failed connection. Redirects are not followed. The last good
envelope is kept on disk and re-verified when read; a 404 clears it,
and an older signed file never replaces a newer one.
The signed sample flamenet.io's builder made is a test fixture, and
LiveTodayTest checks a relay on this machine when FN_LIVE_TODAY_RELAY
names one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
directory-v1.json (DESIGN 5) ships as an asset: 2,964 sites in 90
categories, browsed by category and searched on the phone with no
network. A site sent to a chat is an ordinary text through the 1:1
send path, the URL as the words and an lnk built from the entry: the
title, and desc present and empty (DESIGN 18). Nothing is fetched to
make it, and with link previews off the URL goes alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Headlines, Markets ("Prices are delayed. Not advice."), Weather for
the cities picked on the phone, Site of the day with the file's own
note, the blog when there is a post, and the way into the directory.
The tab asks the relay when it opens and when the app comes back to
it; offline it shows the cached copy with "From <date>", and a 404
shows only the directory and "Today's page is not offered by this
relay." Links open in the browser on a tap, with the host shown.
The dev key (kid a9a94e74) is compiled into debug builds only, from
src/debug; release has its own empty copy, and TodayKeys.PRODUCTION is
empty until the release step. Eight tabs fit a 360dp phone with the
labels one size smaller.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The address card shows the hello link (SPEC 6.34) with Copy and
Share, says what it does, and its QR code holds it.
flamenetmessenger://hi#<address> opens the app and fills in Home's add
screen; nothing is looked up or sent until the person chooses to. Both
scanners read a hello link, an older card or a bare address, and say
so when a code holds none. Parsing follows DESIGN 21: the known link
forms only, the fragment percent-decoded once, fnid: and 52 letters of
the FNID alphabet, a lowercased host with a dot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sender's link preview (§6.12) arrived inside the message and was
never handed to the history store, so no incoming message has shown one
since dc1be35 added them. Directory shares (DESIGN item 18) are sent as
exactly such a preview.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rule (DESIGN 19) has two checks: a relay's tag is kept only when it is
the SHA-256 of the file it came with, and If-None-Match goes out only when
the stored tag is that hash of the copy held. Breaking either one alone left
every test green, because the other still held the behaviour. Now a test
watches each: a foreign tag is not stored, and a foreign tag already on disk
is not sent. Each was seen to fail with its half removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stickers in FlameNet's art, cards, events and answers, games in one-to-one
chats, one-to-one backups keyed by user id, and link previews on incoming
messages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Today tab, the small-web directory with sends to a chat, hello links,
and Home's scanner filling in the address it read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stickers in FlameNet's art, cards, events and answers, games in one-to-one
chats, the Today tab, the small-web directory and hello links.
versionCode and versionName move together, as the comment above them
requires.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Session.absorb handled edits (§6.15), pins (§6.19), reactions (§6.9)
and story reactions (§6.22) inside its loop over renderable messages,
and none of them is renderable, so every one that arrived was dropped:
no reaction, edit or pin from anybody else ever showed on Android. The
tests that existed called applyEdit and the like directly, which is why
they passed.
They are now applied from the whole batch before the renderable filter,
as answers and game moves already are. Each lands only on what it names,
in a group only from one of its members, and none is ever drawn as a
bubble. None is news either, as each of those sections requires: the
thread is redrawn, never marked unread, and nothing notifies. A pin
moves the open chat's banner when it is redrawn.
The test goes through absorb for each of the four, and every one of
them failed before this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 32. The Messages scanner filled the add screen with the handle an
older card named for this relay, which a card could claim for anybody.
Both scanners now fill in fnid@relay through prefillFor, as Home's did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 31. With supervision on, the bar has no Today tab, and a Today
screen left open shows the signed-in home screen instead. The parental
controls copy says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 30. Chess, checkers and reversi turn the board round for whoever
plays second. Tic-tac-toe has no side to face, so its cells now keep their
places on both phones.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 32. TodayData.forget deletes the cached file and the city picks,
and an open Today tab lets go of what it held. Sign-out, the two other
ways out of an account and a lost-device wipe all call it; the wipe
reaches it through Session.onWiped, which the view model and the
background service both set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 29. A new game from the other side notifies "Sam invited you to
play chess" with a sound and marks the chat unread. Moves, draw offers
and endings redraw the board and are said through Session.takeGameNews
without marking anything unread, not even a move that had to rebuild a
lost board, as on the iPhone. An ending keeps its sound; a move and an
offer come without one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner-approved for 0.32. Only a game's news waited for a snooze to end;
an ordinary message in a snoozed chat still rang. NotificationSettings.due
now leaves snoozed chats out of a poll's notification for every kind, and
what arrives still counts as unread.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner-approved for 0.32. A group photo was recorded as pending and never
queued, so it stayed pending for good. It now uploads once and goes
through the fan-out group stickers and cards use, which queues the
members nothing reached with the group thread; the bubble turns delivered
once the last of them has it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner-approved for 0.32. Their secondary lines used the palette's fixed
grey, 1.7:1 on the dark surface. They now take the theme's
onSurfaceVariant, as the game card does, which is the same grey in the
light theme.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 34. Section 8.11 is push-token registration. Every 0.32 citation of
it for the Today file now reads §8.12, and a test keeps it that way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 35. The part before the # was lowercased whole, so /HI was taken,
and the app link allowed a trailing slash. Scheme and host still match in
any case; the path must be /hi or /es/hi exactly, with no trailing slash
and no query.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 36. A rev 1 from somebody other than the stand-in organizer took
over as a revision and notified "<old title> changed". It is now settled:
the card is redrawn and nothing is said. Event and game ids are taken
when they are not empty, a space included, on the wire and off a history
row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 37. A gmove with no log or an empty one, and a gend with no log,
made a board out of nothing or ended one at no position; they are now
dropped. A draw-accept counts only when it carries this phone's whole log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 38. A market entry without sym was dropped. It is now kept; the
row never showed the symbol, so it draws as before. A name and a
non-empty last are still required, as on the iPhone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 38. The envelope's and the body's v went through a number check
that took 1.0 and 1e0, and Android's own parser reads 01 as 1. Both are
now judged on the token as written, so only 1 passes, as on the iPhone
and the relay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 41. A host is checked as ASCII before it is lowercased: Java
lowercases a Kelvin sign to an ASCII k, so a bare or scanned address
holding one was folded into a different host. A fragment over 400
characters is not read, as the relay's /hi page caps it. A typed or
pasted address gets the same host rule. The app link with a slash after
hi, and spaces in a scanned address, were already refused; the test
holds both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 42. A market's sym, last, chg and pct are cut at 24 characters,
and at most five notes are kept. cc is two ASCII letters or is ignored,
tz is ignored past 64 characters, and a zone this phone does not know
shows a city's days as the file gives them, where it used the phone's own
zone to drop days already gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 43. ctl was read as a boolean or as a whole number, so a string
"0", a fraction or an object let a control envelope through as a bubble.
Carrying ctl now means any value but 0, false or null; "1" and true still
count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 44. The game object refused an r that was not a known ending
whatever the kind, so a gmove carrying one was dropped and a start fell
through to the placeholder. Only an ending reads r now; on a start or a
move it is ignored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 45. days above 31 is read as 31 and below 1 as a single day, and
the event composer holds what it writes to the same range.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 46. A secs on a card or an event set that message's own timer.
It is ignored on cards, events and games in 0.32, so they follow the
chat's disappearing timer like the rest of the conversation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One section for stickers, cards, events, games, the Today tab, the
directory and hello links, with the DESIGN item and the iOS file behind
each row, and the sends that can still stay pending for good.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN items 25, 26, 33 and 44, with the iPhone's bytes:
{"fnc":1,"kind":"unpost","ctl":1,"st":1,"sid":"<story id>"}
{"fnc":1,"kind":"unpost","ctl":1,"st":1,"set":"<set.id>"}
{"fnc":1,"kind":"edit","st":1,"caption":"<new words>",
"re":{"mid":"<story id>","author":0,"preview":""}}
The story id is the one §6.20 replies already use here: a picture's
attachment id, a text story's mid.
Taking a story down removes this phone's copy at once, from the strip
and the archive, with its reactions and any decrypted bytes, then tells
everyone it went to: the people recorded when it was posted, now kept
on each story, or for one posted before that or restored from a backup,
a group story's roster or today's story audience, and the change says
which. A set comes down whole by its id. This account's other phones
are told too. Both kinds are silent control traffic and never queued.
A receiver applies either only to a story it holds from the envelope's
sender. An unknown id, a take-down naming both a story and a set or
neither, an edit that would empty a story, and an older edit after a
newer one (by the relay's time) all change nothing. None is drawn and
none notifies. A story's edited time travels in backups; who it was
sent to stays on the phone.
The relay has no route to delete a blob, so a taken-down picture's blob
waits for the retention purge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The feed card cropped every picture to a 240dp band, which is how the
top of a story went missing. A card now takes the picture's own shape,
from two wide by one high to four wide by five high, never taller than
560dp (the iPhone's numbers), and fits the picture whole inside it with
plain bars in the theme's colour beside a tall one or above and below a
wide one. Videos and streams sit in the same frame. The composer draws
the picked picture through the same frame, so it looks exactly as it
will in friends' feeds before Post.
The upload never cropped: pictures are only turned upright, scaled down
whole and re-encoded. The strip on this phone draws initials, not
pictures, so it has no thumbnail to crop.
Under my own story, in one row: Edit caption (Edit words for a text
story) and Take down. Take down asks first, in DESIGN item 25's words.
The editor will not empty a story and offers Take down instead. A
story whose words changed says Edited beside its time left.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 32. A fetch still on its way when the phone was cleared wrote the
file and its state back when it returned, and an open tab showed it. A
feed now belongs to the clear it was made in: one made before a clear
asks nothing and writes nothing, an answer that returns after one is
dropped, and the tab shows nothing from a request that outlived the
clear, as on the iPhone. The count moves before the files go, so a fetch
sees the clear first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stories (and-a, 6dff1f0): incoming edits, pins and reactions applied
from the whole batch; taking a story down and editing its words (§6.35);
a feed card that shows the whole picture.
Both sides kept where they meet: each session is wired with the sealing
policy, the leftover-device retirement, the Today clearing on a wipe and
the attachment cache, in the view model and in the background service.
The Today test that read the one-line wiring now reads each wiring
block. Tic-tac-toe still never turns, and the round 6 and 7 game and ctl
rules stand; the stories branch did not touch either.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner-approved for 0.32. A photo, voice note or file to one person, and a
file to a group, was recorded as pending when it reached nobody and never
queued, so it stayed pending for good. Each is now uploaded once and sent
as its reference: to one person the way a text is, queued when nobody
has a device, and to a group through the fan-out a group photo uses. The
bubble turns delivered once the queued copy goes. The group attachment
send that nothing calls any more is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every kind Android sends, named with the relay's silent flag it goes
with. Edits, reactions, pins and story reactions already went silently
and never notified here, which the owner has now asked of both apps;
the test keeps it so, and makes a new kind a decision. The rule moved
to the companion so it can be tested for what it decides.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner-approved for 0.32. The feed's secondary lines, its tally, its
Mute and Report, its composer notes and its empty-state icon used the
palette's fixed grey, 1.7:1 on the dark surface. They now take the
theme's onSurfaceVariant, as cards, events and polls do; in the light
theme it is the same grey.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A row for edits, reactions, pins and story reactions, which go with the
relay's silent flag and never notify on Android, with where iOS stands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Loader tests for the three vector files flamenet-e2e docs/spec-0-32 adds,
found through FLAMENET_CONTENT_KINDS_VECTORS, FLAMENET_TODAY_VECTORS and
FLAMENET_HELLO_LINK_VECTORS, or in a flamenet-e2e checkout beside this one:
- content kinds (app module): every written envelope parsed and written
back by the engine's encoders, byte for byte; every envelope through
Content.parse and the session's own gates (control traffic, and a move
or an ending through ChatGames.receive), checked field by field.
- Today (client module): TodayFile.verify on the sample, every case and
the 512 KB edge; the sequence and If-None-Match through TodayFeed over
a MockWebServer on this machine.
- hello links (app module): the link cases through HelloLink.fromLink, as
openHelloLink calls it, and the scan cases through prefillFor.
Test files only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Test harness only. Cards, events and answers, games (replayed with the
games engine), photo stories with edits and take-downs, stickers and
directory shares, all built with the engine's own encoders. The bot polls,
logs every envelope it opens, and runs commands dropped in a folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 48. An envelope without v, kid, body or sig, or with an empty kid
or sig, is malformed; Android called a missing signature unsigned and a
missing v an unknown version. A v that is there but not exactly 1 is
still an unknown version, and a body without v is a bad body. The
unsigned reason is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared hello-link vectors trim whitespace around a link, as a
scanned code already was here. Space inside the fragment is still
refused; a newline after the whole link is now whitespace around it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DESIGN 47. A start with stored credentials waited on the relay for the
contact list and the groups before leaving the spinner: up to its call
timeout twice with the network down, and then without any groups, since
those came only from the relay. The chats now open from what is on the
phone, the stored contacts and the groups as last listed, which the
phone now keeps, and the relay's lists replace them when it answers.
The saved Today page already opened from disk. Sign-in still shows only
with no stored credentials.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TEST-A's loaders for content-kinds-v1, today-v1 and hello-link-v1 (4aa0d0f),
test code only. Two lines changed to fit the branch they land on: the
Today test no longer names the unsigned reason, which DESIGN 48 folded
into malformed, and it expects the 34 cases today-v1 holds since the
spec added "envelope version written as a string".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TEST-B's interop peer harness extended to every 0.32 kind, with a bot
mode (1d6de8b). It touches only :interop, not the app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TEST-B finding 5, DESIGN 4. The header showed the plain date whether the
page was the relay's answer or the copy kept on the phone; the "From"
went into the line under it. A kept copy shown because the relay could
not be reached, or sent nothing newer, now reads "From <date>" in the
header and one plain line saying why, as on the iPhone. A page the relay
is serving now keeps the plain date and no line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
.e2e-ref moves to d07ed14 (docs/spec-0-32). Between the old pin and
this one the only existing file that changed is backup-payload-v1.json,
which gains cards, events and games; the rest are new.
unit-tests and vectors-head export FLAMENET_CONTENT_KINDS_VECTORS,
FLAMENET_TODAY_VECTORS and FLAMENET_HELLO_LINK_VECTORS, and the
pre-flight check names them. vectors-head also runs the three loader
classes, which live in :client and :app, not :engine.
The pinned commit has to be on the remote before this pipeline runs, so
the spec branch is pushed first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two 0.32 lines had diverged since 28 September. main carried the Spanish
localization (853 strings) and friend-to-friend bridge sharing;
release/android-0-32 carried 65 commits of stories, Today, the shared test
vectors, the shell peer and envelope wake parity. Neither was a superset, so
this is a real merge of 26 conflicting hunks across 15 files.
.e2e-ref now pins flamenet-e2e 4b4042f, the commit that unified the two spec
lines. Before that no single pin covered both sides. .gitlab-ci.yml takes the
release branch's version, as the owner chose.
Conflicts that were decisions, not text:
- Conversations.kt looked like two versions of uploadAttachment. It was not:
that function exists only on the release branch, and git had paired its
doc comment and parameter list with the body of main's sendGroupAttachment.
Resolving side by side would have deleted one function. Both are written
out whole, and the attachment padding of 23.7 and 23.9 is intact.
- MessengerService and Session: main suppressed alerts for this account's own
device copies (14.5, 17.2); theirs added card, event and game notices with
snooze handling. Session keeps the quiet set and the notice bookkeeping and
returns touched + quiet, so quiet threads still count as unread, and the
service subtracts them before anything is raised.
- MainActivity: contact links and hello links are different schemes, so both
handlers stay. Hello runs first, because helloLinkIn matches only its own
scheme and leaves the intent alone, while the contact handler consumes any
VIEW data and would swallow every hello link.
- MessengerViewModel: fetchedBuddies is not in scope at that point, so the
roster comes from the vault as theirs has it, with the status localized.
Nine string keys to match: seven new (the hello-link QR label, the card and
event forward notes, the Today tab and the three attach row labels) and two
reworded, because supervision now hides the Today tab. English and Spanish
both, so parity holds at 879 keys each side.
No Java on the machine this was merged on, so CI is the first compile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two real defects in 69e12af, both found by the pipeline rather than by reading.
client/Conversations.kt had an extra `}` on line 425. Writing out both
`sendGroupAttachment` (main) and `uploadAttachment` (the release branch) left
the closing brace of the function git had spliced away. The class body closed
at 425 instead of 3466, so the parser reported `companion object` at 3364 as a
syntax error 3000 lines from the cause.
.gitlab-ci.yml took the release branch's copy whole, which predates five
vector exports main had added: padding-v1, sync-merge-v1, handle-cert-v1,
links-v1 and bridge-offer-v1. BridgeLineVectorTest then failed on a missing
file, reading as a protocol bug. The two sides each added different vectors,
so the resolution is the union, in both the pinned job and vectors-head.
vectors-head runs :obfs4:test again for the same reason: it reads the
bridge-offer vectors from SPEC section 24.
All 15 vector files exist at the pinned e2e ref 4b4042f.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Translates .gitlab-ci.yml to .gitea/workflows/ci.yml and deletes it. Kept:
secret sweep, the four-module unit run with --rerun-tasks against the
pinned vectors, the XML floors (595/405/68/515, 1580 total), vectors-head
(advisory).
Runs on mac1/mac2 inside a pinned toolchain image (ci/android-ci.Dockerfile:
Temurin 21.0.5, cmdline-tools 11076708, platform 35, build-tools 34.0.0 and
35.0.0, Node 22.11.0), never on pauls-imac. The `image` job pushes the tag
to git.extant2000.com when it is missing.
Vector and README links now point at git.extant2000.com/extant2000.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The container job picked `sh -e {0}`, and sh on eclipse-temurin:jammy is
dash. Every `set -euo pipefail` step failed on line 2 with "Illegal option
-o pipefail" before running a command, so unit-tests and vectors-head went
red without compiling anything.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Session.kt declared `arrived` twice, once per side. Main's filter is the one
to keep: its own comment records that filtering on `isRenderable` here made
the reaction, edit and pin branches unreachable, so everything that arrived
was decrypted and then dropped. The release branch's declaration is gone and
the loop uses the broader list, which is what main's fix intended.
StoryFrame.kt was extracted from StoriesScreens and localized in the same
move, so it calls `stringResource(R.string.social_story_image_desc)` without
importing either name. Both imports added. The key is in values and values-es
already; parity is 860 keys per language.
SilentKindsTest pinned "bridge offer" to `Content.bridgeOffer(...)`, which
returned String on the release branch and String? on main, where a blank or
over-long line is refused (§24.4). That typed the whole map String? and the
two assertions stopped compiling. The test line is neither blank nor long, so
it is unwrapped at the fixture.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each of these tests was written on the release branch against behaviour main
has since changed. The production behaviour stands and the expectation moves,
because in every case main's rule is the one the spec states.
InboundControlReachesTheThreadTest: a reaction and a pin were expected in
`absorb`'s return. Sections 6.9 and 6.19 say neither may notify, and main
applies both through `changedQuietly`, so the thread is redrawn and never
marked unread. The two tests now assert an empty unread set and the thread in
`takeQuietlyChanged`.
StoryTakeDownTest: the tablet was registered but never linked. Spec 17.3,
which main added, sends an own-account copy only to devices a person linked,
under the key they were linked with. A device signed in with the password is
a device of the account and nothing more. The test links it.
ASnoozedChatHoldsItsNotificationsTest read MessengerService for
`due(touched, ...)`. The service now takes quiet threads out first
(`val loud = touched - s.quietThreads`), per 14.5 and 17.2, and the test
checks both lines.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
StoryTakeDownTest reached for `io.flamenet.e2e.Bytes`, which is internal to
the engine module, so the app's test source set cannot see it. Both that
helper and the client's `Wire.b64` are `Base64.getEncoder().encodeToString`,
which is what the linked-device map is compared against, so the test uses it
directly, as the other app tests do.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two remaining branches in this repository were read and are deleted. Both are already in main by another path.
feat/area-picker (2026-09-10, one commit, "Say when Android is refusing to let a call ring"). Main's NotificationSection.kt already carries it, better: line 87 explains that since Android 14 USE_FULL_SCREEN_INTENT is auto-granted only to apps the system treats as calling apps, line 94 checks canUseFullScreenIntent(), and the notice is localised through set_notify_fsi_title, set_notify_fsi_body and set_notify_fsi_grant. The branch's version was 48 lines of the same with no strings.
4-announce-skips (2026-08-31, "A live suite cannot be pointed at production any more"). Main's LiveTwoProductsTest already points at relay1.flamenet.io and gates on assumeTrue(FN_LIVE_MANAGED == "1") plus a refusal to use a passphrase written into the repository.
The two remaining branches in this repository were read and are deleted. Both are already in main by another path.
`feat/area-picker` (2026-09-10, one commit, "Say when Android is refusing to let a call ring"). Main's NotificationSection.kt already carries it, better: line 87 explains that since Android 14 USE_FULL_SCREEN_INTENT is auto-granted only to apps the system treats as calling apps, line 94 checks `canUseFullScreenIntent()`, and the notice is localised through `set_notify_fsi_title`, `set_notify_fsi_body` and `set_notify_fsi_grant`. The branch's version was 48 lines of the same with no strings.
`4-announce-skips` (2026-08-31, "A live suite cannot be pointed at production any more"). Main's LiveTwoProductsTest already points at relay1.flamenet.io and gates on `assumeTrue(FN_LIVE_MANAGED == "1")` plus a refusal to use a passphrase written into the repository.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes the Android half of the 0.32 unification, and moves CI to Gitea Actions in the same change.
What it does
release/android-0-32intomain. 26 conflict hunks across 15 files, 160 files changed. Replaces Gitea PR #186, which cannot merge.}inclient/Conversations.ktand five vector paths the release branch's CI predated..gitlab-ci.ymlto.gitea/workflows/ci.yml, with the union of both sides' vector sets and the:gamesmodule added.The resolutions worth a human read
Conversations.kt: git paired the release branch's newuploadAttachmentdoc and parameters with main'ssendGroupAttachmentbody. Either side taken whole would have deleted a function, and taking the release side would also have dropped attachment padding, a privacy property under SPEC 23.7 and 23.9. Both functions are written out whole; the padding survives.MainActivity.kt: bothopenLinkIfAskedandopenHelloLinkIfAskedare kept, hello first, becausehelloLinkInfilters byHelloLink.isAppSchemeand leaves the intent alone while the contact handler consumes any VIEWdataString.Session.ktandMessengerService.kt: main'sarrivedfilter fix is kept, and quiet threads are subtracted before the notification decision (val loud = touched - s.quietThreads).Strings
879 keys in English and 879 in Spanish, no key missing from either.
Pin
.e2e-refis4b4042f72880758ee2dea7713f7a2cb63fa12f73, which is now Giteaflamenet-e2emain. All 15 vector files exist at that commit.Not fixed here
DirectoryScreen.kt,GameUi.ktandEventUi.ktstill hold inline English from the release branch. That predates this merge and is out of its scope.A green pipeline proves the tree compiles and the suites pass. It says nothing about whether the notification and padding merges behave as intended, so those deserve a read.
🤖 Generated with Claude Code
Content reads and writes `game`, `gmove` and `gend` exactly as §6.33 shows them: `mine` is the side the sender of that envelope plays, `log` the whole game so far, `r` one of the six endings. A start is drawn as a board in a one-to-one chat only; a game in a group, or one this build cannot read, shows the unsupported placeholder. A move or an ending is control traffic and never a bubble, with or without `ctl`. A backup carries the game on its start message as {"id","t","me","log","r","rby"} (DESIGN items 12 and 14). The out-of-step flag and an open draw offer stay on the phone, each listed with its reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Scanning a code on Home filled the add field with the literal text ${card.fnid}@${card.relay}: the line in130edd7was written with ${'$'} escapes, so the template was never evaluated. Every scan on Home gave the same unusable string. homePrefill() turns a scanned code into the address, bound by the fnid as the audit asked, and Home uses it. The test pins both the address and the absence of the escaped template in MainActivity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Headlines, Markets ("Prices are delayed. Not advice."), Weather for the cities picked on the phone, Site of the day with the file's own note, the blog when there is a post, and the way into the directory. The tab asks the relay when it opens and when the app comes back to it; offline it shows the cached copy with "From <date>", and a 404 shows only the directory and "Today's page is not offered by this relay." Links open in the browser on a tap, with the host shown. The dev key (kid a9a94e74) is compiled into debug builds only, from src/debug; release has its own empty copy, and TodayKeys.PRODUCTION is empty until the release step. Eight tabs fit a 360dp phone with the labels one size smaller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>DESIGN items 25, 26, 33 and 44, with the iPhone's bytes: {"fnc":1,"kind":"unpost","ctl":1,"st":1,"sid":"<story id>"} {"fnc":1,"kind":"unpost","ctl":1,"st":1,"set":"<set.id>"} {"fnc":1,"kind":"edit","st":1,"caption":"<new words>", "re":{"mid":"<story id>","author":0,"preview":""}} The story id is the one §6.20 replies already use here: a picture's attachment id, a text story's mid. Taking a story down removes this phone's copy at once, from the strip and the archive, with its reactions and any decrypted bytes, then tells everyone it went to: the people recorded when it was posted, now kept on each story, or for one posted before that or restored from a backup, a group story's roster or today's story audience, and the change says which. A set comes down whole by its id. This account's other phones are told too. Both kinds are silent control traffic and never queued. A receiver applies either only to a story it holds from the envelope's sender. An unknown id, a take-down naming both a story and a set or neither, an edit that would empty a story, and an older edit after a newer one (by the relay's time) all change nothing. None is drawn and none notifies. A story's edited time travels in backups; who it was sent to stays on the phone. The relay has no route to delete a blob, so a taken-down picture's blob waits for the retention purge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>The container job picked `sh -e {0}`, and sh on eclipse-temurin:jammy is dash. Every `set -euo pipefail` step failed on line 2 with "Illegal option -o pipefail" before running a command, so unit-tests and vectors-head went red without compiling anything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>The two remaining branches in this repository were read and are deleted. Both are already in main by another path.
feat/area-picker(2026-09-10, one commit, "Say when Android is refusing to let a call ring"). Main's NotificationSection.kt already carries it, better: line 87 explains that since Android 14 USE_FULL_SCREEN_INTENT is auto-granted only to apps the system treats as calling apps, line 94 checkscanUseFullScreenIntent(), and the notice is localised throughset_notify_fsi_title,set_notify_fsi_bodyandset_notify_fsi_grant. The branch's version was 48 lines of the same with no strings.4-announce-skips(2026-08-31, "A live suite cannot be pointed at production any more"). Main's LiveTwoProductsTest already points at relay1.flamenet.io and gates onassumeTrue(FN_LIVE_MANAGED == "1")plus a refusal to use a passphrase written into the repository.