Leave and delete group logic

This commit is contained in:
Kgothatso Ngako committed 2026-07-13 00:35:20 +02:00
1 parent 947ae439f0
commit 30db090de8
6 files changed
+106 -123

No files matched your search

@@ -8,7 +8,6 @@ import at.torch.compose.database.model.GiftWrapPayload
import at.torch.compose.database.model.MarmotInnerEvent import at.torch.compose.database.model.MarmotInnerEvent
import at.torch.compose.database.model.MarmotKeyPackage import at.torch.compose.database.model.MarmotKeyPackage
import at.torch.compose.database.model.Participant import at.torch.compose.database.model.Participant
import at.torch.compose.database.model.Profile
import at.torch.compose.database.model.intermdiate.LocalChatMessage import at.torch.compose.database.model.intermdiate.LocalChatMessage
import at.torch.compose.database.model.intermdiate.LocalChatRoom import at.torch.compose.database.model.intermdiate.LocalChatRoom
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
@@ -348,110 +347,46 @@ class DatabaseChatRepository(
} }
} }
override suspend fun leaveChatRoom(localChatRoom: LocalChatRoom) { override suspend fun leaveChatRoom(
val text = "Left group." localChatRoom: LocalChatRoom,
onCompletion: () -> Unit
val mlsGroup = localChatRoom.chatRoom.mlsGroupState?.let { mlsGroupState -> ) {
MlsGroup.restore( database.chatRoomDao().upsert(
MlsGroupState.decodeTls( chatRoom = localChatRoom.chatRoom.copy(
mlsGroupState.hexToByteArray() leftGroupAt = Clock.System.now()
)
) )
} )
if (mlsGroup != null) { // TODO: Process this in the notary...
// Create ChatRumor... sendChatMessage(
val createdAt = Clock.System.now().epochSeconds text = "Left Group",
val marmotInnerEventKind = ChatEvent.KIND localChatRoom = localChatRoom,
// TODO: Add Information/LeaveChatGroup Message Type...
)
val marmotInnerEventId = EventHasher.hashId( onCompletion.invoke()
pubKey = localChatRoom.chatRoom.userPublicKey,
createdAt = createdAt,
tags = emptyArray(),
content = text,
kind = marmotInnerEventKind
)
database.marmotInnerEventDao().upsert(
MarmotInnerEvent(
id = marmotInnerEventId,
publicKey = localChatRoom.chatRoom.userPublicKey,
createdAt = Instant.fromEpochSeconds(createdAt),
tags = emptyArray(),
content = text,
chatRoomId = localChatRoom.chatRoom.id,
kind = marmotInnerEventKind
)
)
database.chatMessageDao().upsert(
ChatMessage(
content = text,
chatRoomId = localChatRoom.chatRoom.id,
senderPublicKey = localChatRoom.chatRoom.userPublicKey,
isUserMessage = true,
giftWrapPayloadId = null,
marmotGroupEventId = null,
marmotInnerEventId = marmotInnerEventId,
)
)
} else {
val innerEventKind = ChatMessageEvent.KIND
// Send GiftWrapMessage
val receiverTags = localChatRoom.localParticipants.filter { participant ->
participant.participant.participantPublicKey != localChatRoom.chatRoom.userPublicKey // TODO: Allow this where it's notes to self...
}.map { localParticipant ->
PTag.assemble(
localParticipant.participant.participantPublicKey,
localParticipant.participant.relayHint?.let { NormalizedRelayUrl(it) }
)
}
val createdAt = Clock.System.now().epochSeconds
val giftWrapPayloadId = EventHasher.hashId(
pubKey = localChatRoom.chatRoom.userPublicKey,
createdAt = createdAt,
tags = receiverTags.toTypedArray(),
content = text,
kind = innerEventKind
)
database.giftWrapPayloadDao().upsert(
GiftWrapPayload(
id = giftWrapPayloadId,
kind = innerEventKind,
tags = receiverTags.toTypedArray(),
createdAt = Instant.fromEpochSeconds(createdAt),
content = text,
chatRoomId = localChatRoom.chatRoom.id,
publicKey = localChatRoom.chatRoom.userPublicKey
)
)
database.chatMessageDao().upsert(
ChatMessage(
content = text,
chatRoomId = localChatRoom.chatRoom.id,
senderPublicKey = localChatRoom.chatRoom.userPublicKey,
isUserMessage = true,
giftWrapPayloadId = giftWrapPayloadId,
marmotGroupEventId = null,
marmotInnerEventId = null
)
)
}
} }
override suspend fun softDeleteChatRoom(chatRoom: ChatRoom) { override suspend fun softDeleteChatRoom(
chatRoom: ChatRoom,
onCompletion: () -> Unit
) {
database.chatRoomDao().upsert( database.chatRoomDao().upsert(
chatRoom.copy( chatRoom.copy(
deletedAt = Clock.System.now() deletedAt = Clock.System.now()
) )
) )
onCompletion.invoke()
} }
override suspend fun deleteChatRoom(chatRoom: ChatRoom) { override suspend fun deleteChatRoom(
chatRoom: ChatRoom,
onCompletion: () -> Unit
) {
database.chatRoomDao().delete(chatRoom) database.chatRoomDao().delete(chatRoom)
onCompletion.invoke()
} }
companion object { companion object {
@@ -4,7 +4,6 @@ import at.torch.compose.database.model.ChatRoom
import at.torch.compose.database.model.GiftWrapPayload import at.torch.compose.database.model.GiftWrapPayload
import at.torch.compose.database.model.MarmotKeyPackage import at.torch.compose.database.model.MarmotKeyPackage
import at.torch.compose.database.model.Participant import at.torch.compose.database.model.Participant
import at.torch.compose.database.model.Profile
import at.torch.compose.database.model.intermdiate.LocalChatMessage import at.torch.compose.database.model.intermdiate.LocalChatMessage
import at.torch.compose.database.model.intermdiate.LocalChatRoom import at.torch.compose.database.model.intermdiate.LocalChatRoom
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -67,11 +66,20 @@ interface ChatRepository {
suspend fun sealGiftWrapPayload(giftWrapPayload: GiftWrapPayload, nostrSignerSync: NostrSignerSync) suspend fun sealGiftWrapPayload(giftWrapPayload: GiftWrapPayload, nostrSignerSync: NostrSignerSync)
suspend fun leaveChatRoom(localChatRoom: LocalChatRoom) suspend fun leaveChatRoom(
localChatRoom: LocalChatRoom,
onCompletion: () -> Unit
)
suspend fun softDeleteChatRoom(chatRoom: ChatRoom) suspend fun softDeleteChatRoom(
chatRoom: ChatRoom,
onCompletion: () -> Unit
)
suspend fun deleteChatRoom(chatRoom: ChatRoom) suspend fun deleteChatRoom(
chatRoom: ChatRoom,
onCompletion: () -> Unit
)
companion object { companion object {
val NO_OP_CHAT_REPOSITORY = object: ChatRepository { val NO_OP_CHAT_REPOSITORY = object: ChatRepository {
@@ -160,15 +168,24 @@ interface ChatRepository {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun leaveChatRoom(localChatRoom: LocalChatRoom) { override suspend fun leaveChatRoom(
localChatRoom: LocalChatRoom,
onCompletion: () -> Unit
) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun softDeleteChatRoom(chatRoom: ChatRoom) { override suspend fun softDeleteChatRoom(
chatRoom: ChatRoom,
onCompletion: () -> Unit
) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun deleteChatRoom(chatRoom: ChatRoom) { override suspend fun deleteChatRoom(
chatRoom: ChatRoom,
onCompletion: () -> Unit
) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }
@@ -63,6 +63,7 @@ fun ChatRoomDetailScreen(
nostrRepository: NostrRepository, nostrRepository: NostrRepository,
chatRepository: ChatRepository, chatRepository: ChatRepository,
onNavigateToRoute: (Route) -> Unit, onNavigateToRoute: (Route) -> Unit,
onPopBackToRoute: (Route) -> Unit
) { ) {
val chatRoomDetailViewModel: ChatRoomDetailViewModel = viewModel( val chatRoomDetailViewModel: ChatRoomDetailViewModel = viewModel(
factory = ChatRoomDetailViewModel.factory( factory = ChatRoomDetailViewModel.factory(
@@ -213,10 +214,9 @@ fun ChatRoomDetailScreen(
), ),
enabled = chatRoomDetailUIState.localChatRoom.chatRoom.leftGroupAt == null, // Disable after we leave group... enabled = chatRoomDetailUIState.localChatRoom.chatRoom.leftGroupAt == null, // Disable after we leave group...
onClick = { onClick = {
// Leave group logic
chatRoomDetailViewModel.leaveGroup( chatRoomDetailViewModel.leaveGroup(
localChatRoom = chatRoomDetailUIState.localChatRoom, localChatRoom = chatRoomDetailUIState.localChatRoom,
onNavigateToRoute = onNavigateToRoute onPopBackToRoute = onPopBackToRoute
) )
} }
) { ) {
@@ -240,10 +240,9 @@ fun ChatRoomDetailScreen(
), ),
enabled = chatRoomDetailUIState.localChatRoom.chatRoom.leftGroupAt != null, enabled = chatRoomDetailUIState.localChatRoom.chatRoom.leftGroupAt != null,
onClick = { onClick = {
onNavigateToRoute.invoke( chatRoomDetailViewModel.softDeleteGroup(
ImplementationPendingRoute( localChatRoom = chatRoomDetailUIState.localChatRoom,
"Delete Group" onPopBackToRoute = onPopBackToRoute
)
) )
} }
) { ) {
@@ -345,7 +344,8 @@ private fun ChatRoomMessagingScreenPreview() {
), ),
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY, nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY, chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY,
onNavigateToRoute = {} onNavigateToRoute = {},
onPopBackToRoute = {}
) )
} }
} }
@@ -51,7 +51,7 @@ import at.torch.compose.ui.composable.navigation.routes.ChatRoomCreationRoute
import at.torch.compose.ui.composable.navigation.routes.ChatRoomDetailRoute import at.torch.compose.ui.composable.navigation.routes.ChatRoomDetailRoute
import at.torch.compose.ui.composable.navigation.routes.ChatRoomMessagingRoute import at.torch.compose.ui.composable.navigation.routes.ChatRoomMessagingRoute
import at.torch.compose.ui.composable.navigation.routes.CreateProfileRoute import at.torch.compose.ui.composable.navigation.routes.CreateProfileRoute
import at.torch.compose.ui.composable.navigation.routes.FeedRoute import at.torch.compose.ui.composable.navigation.routes.HomeRoute
import at.torch.compose.ui.composable.navigation.routes.ImplementationPendingRoute import at.torch.compose.ui.composable.navigation.routes.ImplementationPendingRoute
import at.torch.compose.ui.composable.navigation.routes.KeyPackageManagementRoute import at.torch.compose.ui.composable.navigation.routes.KeyPackageManagementRoute
import at.torch.compose.ui.composable.navigation.routes.LandingRoute import at.torch.compose.ui.composable.navigation.routes.LandingRoute
@@ -444,7 +444,7 @@ fun TorchNavHost(
SocialPreconditionScreen( SocialPreconditionScreen(
onNavigateToSkipForNow = { onNavigateToSkipForNow = {
navController.navigate( navController.navigate(
route = FeedRoute( route = HomeRoute(
activeUserPublicKey = route.activeUserPubkey activeUserPublicKey = route.activeUserPubkey
) )
) )
@@ -465,8 +465,8 @@ fun TorchNavHost(
} }
) )
} }
composable<FeedRoute> { backStackEntry -> composable<HomeRoute> { backStackEntry ->
val route = backStackEntry.toRoute<FeedRoute>() val route = backStackEntry.toRoute<HomeRoute>()
HomeScreen( HomeScreen(
activeUserPublicKey = route.activeUserPublicKey, activeUserPublicKey = route.activeUserPublicKey,
onNavigateToRoute = { eventRoute -> onNavigateToRoute = { eventRoute ->
@@ -594,6 +594,12 @@ fun TorchNavHost(
navController.navigate( navController.navigate(
route = actionRoute route = actionRoute
) )
},
onPopBackToRoute = { popRoute ->
navController.popBackStack(
route = popRoute,
inclusive = false
)
} }
) )
} }
@@ -3,6 +3,6 @@ package at.torch.compose.ui.composable.navigation.routes
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class FeedRoute( data class HomeRoute(
val activeUserPublicKey: String val activeUserPublicKey: String
): Route() ): Route()
@@ -9,11 +9,10 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import at.torch.compose.database.model.ChatRoom
import at.torch.compose.database.model.intermdiate.LocalChatRoom import at.torch.compose.database.model.intermdiate.LocalChatRoom
import at.torch.compose.repository.ChatRepository import at.torch.compose.repository.ChatRepository
import at.torch.compose.repository.NostrRepository import at.torch.compose.repository.NostrRepository
import at.torch.compose.ui.composable.navigation.routes.ImplementationPendingRoute import at.torch.compose.ui.composable.navigation.routes.HomeRoute
import at.torch.compose.ui.composable.navigation.routes.Route import at.torch.compose.ui.composable.navigation.routes.Route
import at.torch.compose.ui.view.state.ChatRoomDetailUIState import at.torch.compose.ui.view.state.ChatRoomDetailUIState
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
@@ -46,6 +45,7 @@ class ChatRoomDetailViewModel(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val localChatRoom = chatRepository.getChatRoomByIdentifier(chatRoomId) val localChatRoom = chatRepository.getChatRoomByIdentifier(chatRoomId)
logger.d("localChatRoom: $localChatRoom")
chatRoomDetailUIState = if (localChatRoom == null) { chatRoomDetailUIState = if (localChatRoom == null) {
// Initiate new chat... and navigate to new chat... // Initiate new chat... and navigate to new chat...
ChatRoomDetailUIState.Error("Missing chat room") ChatRoomDetailUIState.Error("Missing chat room")
@@ -59,20 +59,45 @@ class ChatRoomDetailViewModel(
fun leaveGroup( fun leaveGroup(
localChatRoom: LocalChatRoom, localChatRoom: LocalChatRoom,
onNavigateToRoute: (Route) -> Unit onPopBackToRoute: (Route) -> Unit
) { ) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
chatRepository.leaveChatRoom(localChatRoom) chatRepository.leaveChatRoom(
localChatRoom,
viewModelScope.launch(Dispatchers.Main) { onCompletion = {
onNavigateToRoute.invoke( viewModelScope.launch(Dispatchers.Main) {
ImplementationPendingRoute( onPopBackToRoute.invoke(
"Leave Group" HomeRoute(
) activeUserPublicKey = activeUserPublicKey,
) )
} )
}
}
)
} }
} }
fun softDeleteGroup(
localChatRoom: LocalChatRoom,
onPopBackToRoute: (Route) -> Unit
) {
viewModelScope.launch(Dispatchers.IO) {
chatRepository.softDeleteChatRoom(
chatRoom = localChatRoom.chatRoom,
onCompletion = {
viewModelScope.launch(Dispatchers.Main) {
onPopBackToRoute.invoke(
HomeRoute(
activeUserPublicKey = activeUserPublicKey,
)
)
}
}
)
}
}
companion object { companion object {
private const val TAG = "ChatRoomDetailViewModel" private const val TAG = "ChatRoomDetailViewModel"