Invite member to group

This commit is contained in:
Kgothatso Ngako
2026-07-10 22:39:05 +02:00
parent 4668b36d5f
commit 13485e5606
6 changed files with 171 additions and 120 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 = {}
)
}
}

View File

@@ -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
)
},
)
}

View File

@@ -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,
)
}
}

View File

@@ -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(