Kgothatso Ngako c5c4bc9ed6 feat: pick group members before creating a chat room
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>
2026-08-29 16:14:37 +02:00
2026-07-05 23:21:02 +02:00
2026-07-28 09:10:20 +02:00
2026-03-24 04:47:19 +02:00
2026-03-23 01:41:39 +02:00
2026-03-23 01:41:39 +02:00
2026-03-23 01:41:39 +02:00

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 thats 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 Apples 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 youre 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 IDEs 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 IDEs 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 IDEs toolbar or open the /iosApp directory in Xcode and run it from there.


Learn more about Kotlin Multiplatform

Description
Mantra as a kotlin multiplatform project
https://mantra.press
Readme 8.2 MiB
Languages
Kotlin 100%