Phase 5 of docs/member-archive.md, and the half that makes the whole thing reliable. A device opening a room it holds no signed work for asks the group; any member holding the work answers, addressed to whoever asked. **A request cannot lose the race a push loses.** Pushing an archive at an invitee is an application message in the epoch the add created, and one that overtakes the Welcome is dropped rather than deferred -- silently, while the inviter sees a success. That is marmot-membership.md's failure mode arriving in a new costume. Sending a request cannot lose it, because being able to send one is the proof it was won: a device that can put an application message into the room has processed its Welcome and is at the group's epoch. It also covers three things no invite-time push reaches, and they are answered by one rule because they are indistinguishable from inside the database: a member added after the work, a reinstall whose invite is long past, and a second device that was never invited at all. Hence the crude condition -- no dialects and no artifacts -- rather than anything that tries to tell them apart. **Anyone may answer and nobody is elected to.** A duplicate answer costs bandwidth and nothing else: pages are idempotent and every member who is not the named recipient ignores them. So the stand-down that would avoid the waste is an optimisation to add later rather than a correctness gap to close now. A member with nothing signed answers nothing at all, which is the honest reply from one still catching up themselves, and beats an empty archive that looks like an answer. **Queued with no chat line**, the way a signing message travels. Broadcast does not depend on one -- `encryptAndSendMarmotInnerEvent` inserts its `BroadcastNostrEventRequest` unconditionally, which is what let the FROST rounds travel with no transcript -- and an archive that filed a line per page would put a row of envelopes in the room's history. One line per archive is the right number and it is not writable from here, since the pages are indistinguishable from each other at this point. **Schema 11 -> 12**: `ChatRoom.archiveRequestedAt`, nullable, so Room generates the migration. It stops a device asking again on every launch while an answer is in flight. Rooms written before it read back null, meaning "never asked", which is true of all of them and harmless. Cleared as soon as an archive applies anything -- not when a sender's page count claims the archive was complete. A page count is the sender's word about the transfer rather than about the group's record, so a member who left work out must not get the last word on whether to ask again. A partial answer is followed by another request rather than by silence. The trigger is opening the room, through `ChatRepository` rather than from the view model into the database. Cheap to call every time: it stops at a room that already holds work and at one still waiting. A failure is not reported, because nothing acknowledges a request and the next open asks again. Eight tests: a device with nothing asks and does not ask twice, a device with work does not ask, answering queues pages addressed to the asker with every archivable kind in them and no chat line, a member with nothing signed answers nothing, a member does not answer themselves, an applied archive clears the stamp so a partial answer can be followed up, and the whole round trip -- ask, answer, apply -- leaves the joiner holding the sender's rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This is a Kotlin Multiplatform project targeting Android, iOS, Desktop (JVM).
-
/composeApp is for code that will be shared across your Compose Multiplatform applications. It contains several subfolders:
- commonMain is for code that’s common for all targets.
- Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, the iosMain folder would be the right place for such calls. Similarly, if you want to edit the Desktop (JVM) specific part, the jvmMain folder is the appropriate location.
-
/iosApp contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.
Build and Run Android Application
To build and run the development version of the Android app, use the run configuration from the run widget in your IDE’s toolbar or build it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:assembleDebug - on Windows
.\gradlew.bat :composeApp:assembleDebug
Build and Run Desktop (JVM) Application
To build and run the development version of the desktop app, use the run configuration from the run widget in your IDE’s toolbar or run it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:run - on Windows
.\gradlew.bat :composeApp:run
Build and Run iOS Application
To build and run the development version of the iOS app, use the run configuration from the run widget in your IDE’s toolbar or open the /iosApp directory in Xcode and run it from there.
Learn more about Kotlin Multiplatform…