From 13485e560629096b4cc5d10e1f18b00a0946ab3b Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Fri, 10 Jul 2026 22:39:05 +0200 Subject: [PATCH] Invite member to group --- .../repository/DatabaseChatRepository.kt | 11 ++ .../compose/repository/ChatRepository.kt | 14 ++ .../AddMemberToChatRoomConfirmationScreen.kt | 113 +++++++++++++--- .../ui/composable/navigation/TorchNavHost.kt | 18 +-- ...ddMemberToChatRoomConfirmationViewModel.kt | 128 ++++++------------ .../AddMemberToChatRoomConfirmationUIState.kt | 7 +- 6 files changed, 171 insertions(+), 120 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseChatRepository.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseChatRepository.kt index d5c60280..424bdf75 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseChatRepository.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/database/repository/DatabaseChatRepository.kt @@ -129,6 +129,17 @@ class DatabaseChatRepository( ) } + override suspend fun inviteMember( + localChatRoom: LocalChatRoom, + peerPublicKey: HexKey, + peerKeyPackage: MarmotKeyPackage + ) { + database.marmotOutboundDao().inviteMember( + localChatRoom = localChatRoom, + peerPublicKey = peerPublicKey, + peerKeyPackage = peerKeyPackage + ) + } override suspend fun sendChatMessage( text: String, localChatRoom: LocalChatRoom, diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/repository/ChatRepository.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/repository/ChatRepository.kt index 39f55a57..deb23ee8 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/repository/ChatRepository.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/repository/ChatRepository.kt @@ -50,6 +50,12 @@ interface ChatRepository { peerKeyPackage: MarmotKeyPackage ): String + suspend fun inviteMember( + localChatRoom: LocalChatRoom, + peerPublicKey: HexKey, + peerKeyPackage: MarmotKeyPackage, + ) + suspend fun sendChatMessage( text: String, localChatRoom: LocalChatRoom, @@ -119,6 +125,14 @@ interface ChatRepository { TODO("Not yet implemented") } + override suspend fun inviteMember( + localChatRoom: LocalChatRoom, + peerPublicKey: HexKey, + peerKeyPackage: MarmotKeyPackage + ) { + TODO("Not yet implemented") + } + override suspend fun sendChatMessage( text: String, localChatRoom: LocalChatRoom, diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/AddMemberToChatRoomConfirmationScreen.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/AddMemberToChatRoomConfirmationScreen.kt index 8c01cd28..6d7d576e 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/AddMemberToChatRoomConfirmationScreen.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/AddMemberToChatRoomConfirmationScreen.kt @@ -7,13 +7,17 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment @@ -23,6 +27,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import at.torch.compose.database.model.ChatRoom +import at.torch.compose.database.model.MarmotKeyPackage +import at.torch.compose.database.model.Profile import at.torch.compose.database.model.intermdiate.LocalChatRoom import at.torch.compose.repository.ChatRepository import at.torch.compose.repository.NostrRepository @@ -37,16 +43,18 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey @Composable fun AddMemberToChatRoomConfirmationScreen( activeUserPublicKey: HexKey, - chatRoomId: String, + chatRoomId: HexKey, + profilePublicKey: HexKey, relayHint: String?, initialChatRoomDetailUIState: AddMemberToChatRoomConfirmationUIState = AddMemberToChatRoomConfirmationUIState.Loading, nostrRepository: NostrRepository, chatRepository: ChatRepository, - onNavigateToChat: (Route) -> Unit, + onInviteSent: () -> Unit, ) { val addMemberToChatRoomConfirmationViewModel: AddMemberToChatRoomConfirmationViewModel = viewModel( factory = AddMemberToChatRoomConfirmationViewModel.factory( chatRoomId = chatRoomId, + profilePublicKey = profilePublicKey, relayHint = relayHint, initialAddMemberToChatRoomConfirmationUIState = initialChatRoomDetailUIState, nostrRepository = nostrRepository, @@ -58,7 +66,7 @@ fun AddMemberToChatRoomConfirmationScreen( when (val addMemberToChatRoomConfirmationUIState = addMemberToChatRoomConfirmationViewModel.addMemberToChatRoomConfirmationUIState) { is AddMemberToChatRoomConfirmationUIState.Error -> { Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Spacer( @@ -66,24 +74,51 @@ fun AddMemberToChatRoomConfirmationScreen( ) Text( text = addMemberToChatRoomConfirmationUIState.message, + textAlign = TextAlign.Center ) } } is AddMemberToChatRoomConfirmationUIState.Loaded -> { Scaffold( - topBar = { - TopAppBar( - title = { - val title = if (addMemberToChatRoomConfirmationUIState.localChatRoom.chatRoom.subject != null) { - "Invite new member to ${addMemberToChatRoomConfirmationUIState.localChatRoom.chatRoom.subject}" - } else { - "Invite new member to chat" +// topBar = { +// TopAppBar( +// title = { +// val title = if (addMemberToChatRoomConfirmationUIState.localChatRoom.chatRoom.subject != null) { +// "Invite new member to ${addMemberToChatRoomConfirmationUIState.localChatRoom.chatRoom.subject}" +// } else { +// "Invite new member to chat" +// } +// Text( +// text = title, +// ) +// }, +// actions = { +// } +// ) +// } + bottomBar = { + BottomAppBar( + floatingActionButton = { + ExtendedFloatingActionButton( + onClick = { + addMemberToChatRoomConfirmationViewModel.inviteToChatRoom( + localChatRoom = addMemberToChatRoomConfirmationUIState.localChatRoom, + peerPublicKey = addMemberToChatRoomConfirmationUIState.profile.publicKey, + peerKeyPackage = addMemberToChatRoomConfirmationUIState.marmotKeyPackage, + onInviteSent = onInviteSent + ) + } + ) { + Icon( + Icons.Default.Add, + contentDescription = "Invite new member" + ) + + Text("Invite ${addMemberToChatRoomConfirmationUIState.profile.humanReadableNameOrPubkey()}") } - Text( - text = title, - ) }, actions = { + } ) } @@ -92,11 +127,32 @@ fun AddMemberToChatRoomConfirmationScreen( modifier = Modifier.padding(innerPadding).fillMaxSize() ) { Column( - modifier = Modifier.weight(1f).fillMaxWidth(), + modifier = Modifier.weight(1f).fillMaxWidth().padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp) ) { - Text("Render confirmation") + Spacer( + modifier = Modifier.weight(1f) + ) + + Text("Invite") + + Text( + text = addMemberToChatRoomConfirmationUIState.profile.humanReadableNameOrPubkey() + ) + + Text( + text = "to join the ${addMemberToChatRoomConfirmationUIState.localChatRoom.chatRoom.subject ?: ""} chat room." + ) + + Text( + text = "Once invited will be able to receive and send private message sent to all the ${addMemberToChatRoomConfirmationUIState.localChatRoom.localParticipants.size} members in the chat room.", + textAlign = TextAlign.Center + ) + + Spacer( + modifier = Modifier.weight(3f) + ) } } } @@ -152,22 +208,35 @@ private fun ChatRoomDetailScreenPreview() { ) { AddMemberToChatRoomConfirmationScreen( activeUserPublicKey = "", + profilePublicKey = "hex", chatRoomId = "publicKey", relayHint = null, - initialChatRoomDetailUIState = AddMemberToChatRoomConfirmationUIState.Loaded( + initialChatRoomDetailUIState = +// AddMemberToChatRoomConfirmationUIState.Error("Profile missing key package event.\n\nTry again later or tell Person to use Torch."), + AddMemberToChatRoomConfirmationUIState.Loaded( localChatRoom = LocalChatRoom( chatRoom = ChatRoom( - id = "", - userPublicKey = "", - subject = "Message title", + id = "hex", + userPublicKey = "hex", + subject = "Room Title", initialGiftWrapPayloadId = "sdfaer", mlsGroupState = null ), - ) + ), + profile = Profile( + publicKey = "hex", + nostrEventId = "hex", + displayName = "Person" + ), + marmotKeyPackage = MarmotKeyPackage( + id = "keypackage", + publicKey = "hex", + tlsEncodedMarmotKeyPackage = ByteArray(0) + ) ), nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY, chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY, - onNavigateToChat = {} + onInviteSent = {} ) } } diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/TorchNavHost.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/TorchNavHost.kt index 3c2320c9..b180acf2 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/TorchNavHost.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/composable/navigation/TorchNavHost.kt @@ -596,17 +596,19 @@ fun TorchNavHost( AddMemberToChatRoomConfirmationScreen( activeUserPublicKey = route.activeUserPublicKey, chatRoomId = route.chatRoomId, + profilePublicKey = route.profilePublicKey, relayHint = route.relayHint, nostrRepository = databaseNostrRepository, chatRepository = databaseChatRepository, - onNavigateToChat = { chatRoomDetailRoute -> - navController.navigate( - route = chatRoomDetailRoute - ) { - popUpTo(route) { - inclusive = true - } - } + onInviteSent = { + navController.popBackStack( + route = SearchMemberToAddToChatRoomRoute( + activeUserPublicKey = route.activeUserPublicKey, + chatRoomId = route.chatRoomId, + relayHint = route.relayHint + ), + inclusive = true + ) }, ) } diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/AddMemberToChatRoomConfirmationViewModel.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/AddMemberToChatRoomConfirmationViewModel.kt index db1c5581..81b751d2 100644 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/AddMemberToChatRoomConfirmationViewModel.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/model/AddMemberToChatRoomConfirmationViewModel.kt @@ -9,10 +9,10 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory +import at.torch.compose.database.model.MarmotKeyPackage +import at.torch.compose.database.model.intermdiate.LocalChatRoom import at.torch.compose.repository.ChatRepository import at.torch.compose.repository.NostrRepository -import at.torch.compose.ui.composable.navigation.routes.ChatRoomDetailRoute -import at.torch.compose.ui.composable.navigation.routes.Route import at.torch.compose.ui.view.state.AddMemberToChatRoomConfirmationUIState import co.touchlab.kermit.Logger import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -23,6 +23,7 @@ import kotlinx.coroutines.launch class AddMemberToChatRoomConfirmationViewModel( val chatRoomId: String, val activeUserPublicKey: HexKey, + val profilePublicKey: HexKey, val relayHint: String?, initialAddMemberToChatRoomConfirmationUIState: AddMemberToChatRoomConfirmationUIState, val nostrRepository: NostrRepository, @@ -42,108 +43,56 @@ class AddMemberToChatRoomConfirmationViewModel( viewModelScope.launch(Dispatchers.IO) { val localChatRoom = chatRepository.getChatRoomByIdentifier(chatRoomId) - addMemberToChatRoomConfirmationUIState = if (localChatRoom == null) { - // Initiate new chat... and navigate to new chat... - AddMemberToChatRoomConfirmationUIState.Error("Couldn't load chat") - } else { - AddMemberToChatRoomConfirmationUIState.Loaded( - localChatRoom = localChatRoom - ) - } - } - } + addMemberToChatRoomConfirmationUIState = if (localChatRoom != null) { + val profile = chatRepository.getProfileWithPublicKey(profilePublicKey) - fun initiateNewChat( - onNavigateToChat: (Route) -> Unit - ) { - logger.d("initiateNewChat: $chatRoomId") - // Get ChatRoomById - viewModelScope.launch(Dispatchers.IO) { + if (profile != null) { + val marmotKeyPackage = chatRepository.getMarmotKeyPackageForPublicKey(profilePublicKey) - // Check if we have a chat with activeUserPublicKey and chatRoomId (pubkey) as participants - val participants = chatRepository.getAllParticipantsWithPubKey(chatRoomId) - - participants.forEach { participant -> - chatRepository.getChatRoomByIdentifier(participant.chatRoomId)?.let { localChatRoom -> - // TODO: Check that chatRoom is not a group chat... - if (localChatRoom.localParticipants.size == 1) { - // This is a chat with self... - // TODO: navigateToTheChat... - viewModelScope.launch(Dispatchers.Main) { - onNavigateToChat.invoke( - ChatRoomDetailRoute( - activeUserPublicKey = activeUserPublicKey, - chatRoomId = localChatRoom.chatRoom.id, - relayHint = relayHint - ) - ) - } - return@launch - - } else if (localChatRoom.localParticipants.size == 2) { - val activeUserParticipant = localChatRoom.localParticipants.find { localParticipant -> localParticipant.participant.participantPublicKey == activeUserPublicKey } - - if (activeUserParticipant != null) { - // We have found the chat we are looking for... - viewModelScope.launch(Dispatchers.Main) { - onNavigateToChat.invoke( - ChatRoomDetailRoute( - activeUserPublicKey = activeUserPublicKey, - chatRoomId = localChatRoom.chatRoom.id, - relayHint = relayHint - ) - ) - } - return@launch - } + if (marmotKeyPackage != null) { + AddMemberToChatRoomConfirmationUIState.Loaded( + localChatRoom = localChatRoom, + profile = profile, + marmotKeyPackage = marmotKeyPackage + ) + } else { + AddMemberToChatRoomConfirmationUIState.Error("Profile missing key package event. Try again later or tell ${profile.humanReadableNameOrPubkey()} to use Torch.") } - } - } - - val profile = chatRepository.getProfileWithPublicKey(chatRoomId) - - if (profile != null) { - logger.d("Create chat with: $profile") - // Check if we have a keyPackageEvent... - val keyPackageEvent = chatRepository.getMarmotKeyPackageForPublicKey(chatRoomId) - - if (keyPackageEvent == null) { - addMemberToChatRoomConfirmationUIState = AddMemberToChatRoomConfirmationUIState.Error("Missing Key Package") // TODO: Introduce missing key package UI State... } else { - // Create new chat and invite local - try { - val chatRoomId = chatRepository.createMlsDirectMessage( - userPublicKey = activeUserPublicKey, - peerPublicKey = chatRoomId, - peerKeyPackage = keyPackageEvent, - ) - - onNavigateToChat.invoke( - ChatRoomDetailRoute( - activeUserPublicKey = activeUserPublicKey, - chatRoomId = chatRoomId, - relayHint = relayHint - ) - ) - } catch (e: Throwable) { - addMemberToChatRoomConfirmationUIState = AddMemberToChatRoomConfirmationUIState.Error("Failed to create chat") - - } + AddMemberToChatRoomConfirmationUIState.Error("Failed to find profile...") } - } else { - addMemberToChatRoomConfirmationUIState = AddMemberToChatRoomConfirmationUIState.Error("Couldn't find profile...") + AddMemberToChatRoomConfirmationUIState.Error("Couldn't load chat") } } } + fun inviteToChatRoom( + localChatRoom: LocalChatRoom, + peerPublicKey: HexKey, + peerKeyPackage: MarmotKeyPackage, + onInviteSent: () -> Unit + ) { + viewModelScope.launch(Dispatchers.IO) { + // Invite member + chatRepository.inviteMember( + localChatRoom = localChatRoom, + peerPublicKey = peerPublicKey, + peerKeyPackage = peerKeyPackage + ) + viewModelScope.launch(Dispatchers.Main) { + onInviteSent.invoke() + } + } + } companion object { private const val TAG = "AddMemberToChatRoomConfirmationViewModel" fun factory( activeUserPublicKey: HexKey, - chatRoomId: String, + profilePublicKey: HexKey, + chatRoomId: HexKey, relayHint: String?, initialAddMemberToChatRoomConfirmationUIState: AddMemberToChatRoomConfirmationUIState = AddMemberToChatRoomConfirmationUIState.Loading, nostrRepository: NostrRepository, @@ -153,10 +102,11 @@ class AddMemberToChatRoomConfirmationViewModel( AddMemberToChatRoomConfirmationViewModel( activeUserPublicKey = activeUserPublicKey, chatRoomId = chatRoomId, + profilePublicKey = profilePublicKey, relayHint = relayHint, initialAddMemberToChatRoomConfirmationUIState = initialAddMemberToChatRoomConfirmationUIState, nostrRepository = nostrRepository, - chatRepository = chatRepository + chatRepository = chatRepository, ) } } diff --git a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/state/AddMemberToChatRoomConfirmationUIState.kt b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/state/AddMemberToChatRoomConfirmationUIState.kt index 555a6ac3..2f7cd429 100755 --- a/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/state/AddMemberToChatRoomConfirmationUIState.kt +++ b/composeApp/src/commonMain/kotlin/at/torch/compose/ui/view/state/AddMemberToChatRoomConfirmationUIState.kt @@ -1,10 +1,15 @@ package at.torch.compose.ui.view.state +import at.torch.compose.database.model.MarmotKeyPackage +import at.torch.compose.database.model.Profile import at.torch.compose.database.model.intermdiate.LocalChatRoom +import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent sealed interface AddMemberToChatRoomConfirmationUIState { data class Loaded( - val localChatRoom: LocalChatRoom + val localChatRoom: LocalChatRoom, + val profile: Profile, + val marmotKeyPackage: MarmotKeyPackage ): AddMemberToChatRoomConfirmationUIState data class Error(