Group creation was a single screen: name + description, then "Create
chat" minted the MLS group and dropped the user straight into an empty
room, with no way to bring anyone along except the existing
one-at-a-time invite path (chat room detail -> search member ->
confirm). Add a second step that lists the profiles already in the
local store and lets the user tick everyone the group is for, so a
group is created together with its members.
The flow is now:
ChatRoomCreationRoute (name + description)
-> SelectChatRoomMembersRoute (pick people, create, invite)
-> ChatRoomMessagingRoute
New: ui/composable/navigation/routes/SelectChatRoomMembersRoute.kt
* Carries activeUserPublicKey plus the name/description gathered by step
one, so nothing is persisted until the user confirms who is in.
* `description` is nullable with a default, and ChatRoomCreationViewModel
maps blank text onto null: an empty string is not something navigation
round-trips reliably as a path argument.
New: ui/view/state/SelectChatRoomMembersUIState.kt
* The Loading/Loaded/Error triple the other chat screens use. Loaded
carries the pickable profiles.
New: ui/view/model/SelectChatRoomMembersViewModel.kt
* initiate() lists nostrRepository.searchableProfiles() excluding the
active user -- purely local rows, no directory lookup. It also queues a
negentropy sync for every listed profile's KeyPackageEvent up front, so
the packages needed to actually add anybody have usually landed by the
time the user has finished ticking names.
* createChatRoom() moves here from ChatRoomCreationViewModel, unchanged
in substance: bootstrap MarmotGroupData into the epoch-0 GroupContext,
MlsGroup.create, then getOrCreateChatRoom keyed on the Marmot
nostr_group_id (not MlsGroup's own groupId). Minting the group here
rather than in step one is the point of the split -- backing out of the
picker no longer strands a member-less room in the chat list.
* inviteSelectedMembers() resolves the selected members' key packages
concurrently under one shared 20s budget (a single relay round trip for
the batch instead of one timeout per member) by observing
observeMarmotKeyPackageForPublicKey, then invites sequentially. The
room is re-read from the repository before every invite: inviteMember
advances the MLS epoch and persists the new state, so reusing the
snapshot taken before the previous invite would build the next commit
on top of state the group has already left.
* A member whose key package never shows up does not sink the group. It
is created without them and the screen names who was left out;
createdChatRoomId then turns the action into "Open chat" against the
room that already exists rather than minting a second one, and
toggleMember is frozen once the room exists so further ticks cannot
look like they will still be honoured.
New: ui/composable/SelectChatRoomMembersScreen.kt
* Checkbox list over Loaded.profiles, reusing the row shape of
SearchMemberToAddToChatRoomScreen (ProfileAvatar + name + about); both
the row and the checkbox toggle selection.
* BottomAppBar carries the running selection count and an
ExtendedFloatingActionButton labelled "Create chat with N", which
swaps to a progress indicator while the group is being built.
* An empty local store gets an explanatory state that still allows
creating the chat and inviting people later.
ChatRoomCreationViewModel
* Reduced to the details form. Group creation, the wallet keypair and
both repositories move to the picker, so factory() now takes no
arguments at all.
* Gains validateInput() (mirroring CreateProfileViewModel) so an unnamed
chat cannot advance, and selectMembers() to hand the collected
name/description to the next route.
ChatRoomCreationScreen
* Takes activeUserPublicKey from the route instead of the wallet flow
and the two repositories; the button becomes "Choose who to chat
with".
* Fix the "groupd" typo in the name placeholder.
MantraNavHost
* Register SelectChatRoomMembersRoute. Opening the finished chat pops
back through ChatRoomCreationRoute inclusive, so backing out of a
brand new chat lands where the user started rather than in the
half-filled creation form.
Known limits: the picker inherits ProfileDao's default LIMIT 21, so only
the first 21 local profiles are offered (the same cap the existing
add-member search already lives with), and a selected profile can only
join if their KeyPackageEvent is reachable -- contacts who have never
published one always land in the "couldn't be added" list.
Verified:
./gradlew :composeApp:compileCommonMainKotlinMetadata
./gradlew :composeApp:compileDebugKotlinAndroid
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…