Sync MLS group messages

This commit is contained in:
Kgothatso Ngako committed 2026-07-02 20:34:21 +02:00
1 parent 2210a0e28b
commit 0a5a2194c4
7 files changed
+90 -50

No files matched your search

@@ -20,6 +20,7 @@ import at.torch.compose.exceptions.GiftWrapImpersonationException
import at.torch.compose.exceptions.GiftWrapSealDecryptionException import at.torch.compose.exceptions.GiftWrapSealDecryptionException
import at.torch.compose.exceptions.GiftWrapUnsealException import at.torch.compose.exceptions.GiftWrapUnsealException
import at.torch.compose.exceptions.MarmotMissingKeyPackageBundleException import at.torch.compose.exceptions.MarmotMissingKeyPackageBundleException
import at.torch.compose.exceptions.MarmotMissingNostrGroupDataExtension
import at.torch.compose.exceptions.MarmotWelcomeEventMissingKeyPackageEventIdException import at.torch.compose.exceptions.MarmotWelcomeEventMissingKeyPackageEventIdException
import at.torch.compose.extensions.toHex import at.torch.compose.extensions.toHex
import at.torch.compose.network.serialization.encodeToJsonString import at.torch.compose.network.serialization.encodeToJsonString
@@ -615,7 +616,7 @@ abstract class NostrDao(
) )
logger.d("Group: $group") logger.d("Group: $group")
val nostrGroupId = group.groupId.toHex() val nostrGroupId = group.currentMarmotData()?.nostrGroupId ?: throw MarmotMissingNostrGroupDataExtension("Didn't find group data for $group")
logger.d("nostrGroupId: $nostrGroupId") logger.d("nostrGroupId: $nostrGroupId")
@@ -78,8 +78,8 @@ data class ChatRoom(
): at.torch.compose.database.model.traits.TimestampedEntity, ): at.torch.compose.database.model.traits.TimestampedEntity,
at.torch.compose.database.model.traits.LocalStoreEntity, at.torch.compose.database.model.traits.LocalStoreEntity,
at.torch.compose.database.model.traits.UserViewableEntity, at.torch.compose.database.model.traits.UserViewableEntity,
at.torch.compose.database.model.traits.SoftDeletableEntity { at.torch.compose.database.model.traits.SoftDeletableEntity
{
companion object { companion object {
const val TAG = "ChatRoom" const val TAG = "ChatRoom"
@@ -185,10 +185,6 @@ fun HomeScreen(
factory = at.torch.compose.ui.view.model.ChatRoomListViewModel.factory( factory = at.torch.compose.ui.view.model.ChatRoomListViewModel.factory(
initialChatRoomListUIState = at.torch.compose.ui.view.state.ChatRoomListUIState.Loading, initialChatRoomListUIState = at.torch.compose.ui.view.state.ChatRoomListUIState.Loading,
publicKey = homeScreenUIState.profileWithFollowing.profile.publicKey, publicKey = homeScreenUIState.profileWithFollowing.profile.publicKey,
synchronizationFilter = homeScreenViewModel.getSynchronizationFilter(
homeScreenTypeType,
profileWithFollowing = homeScreenUIState.profileWithFollowing
),
nostrRepository = nostrRepository, nostrRepository = nostrRepository,
chatRepository = chatRepository chatRepository = chatRepository
), ),
@@ -164,7 +164,7 @@ fun ShareProfileScreen(
clipboardManager.setText( clipboardManager.setText(
buildAnnotatedString { buildAnnotatedString {
activeUserPublicKey.hexToNpubHrp() append(activeUserPublicKey.hexToNpubHrp())
} }
) )
} }
@@ -25,15 +25,23 @@ 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.NegentropySynchronizeRequest
import at.torch.compose.database.model.types.SynchronizationFilter
import at.torch.compose.nostr.Relays
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroup
import com.vitorpamplona.quartz.marmot.mls.group.MlsGroupState
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import io.ktor.utils.io.core.toByteArray
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.log
class ChatRoomListViewModel( class ChatRoomListViewModel(
initialChatRoomListUIState: at.torch.compose.ui.view.state.ChatRoomListUIState, initialChatRoomListUIState: at.torch.compose.ui.view.state.ChatRoomListUIState,
val publicKey: String, val publicKey: String,
val synchronizationFilter: at.torch.compose.database.model.types.SynchronizationFilter,
val nostrRepository: at.torch.compose.repository.NostrRepository, val nostrRepository: at.torch.compose.repository.NostrRepository,
val chatRepository: at.torch.compose.repository.ChatRepository val chatRepository: at.torch.compose.repository.ChatRepository
): ViewModel() { ): ViewModel() {
@@ -65,20 +73,78 @@ class ChatRoomListViewModel(
logger.d("scheduleSynchronization") logger.d("scheduleSynchronization")
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
// Sync Notifications... might want to also run this in the background // Sync Notifications... might want to also run this in the background
val chatRequestFilter = SynchronizationFilter(
kinds = arrayOf(
GiftWrapEvent.KIND,
),
tags = mapOf(
Pair("p", listOf(publicKey))
),
limit = 50
)
nostrRepository.queueNegentropySynchronizeRequest( nostrRepository.queueNegentropySynchronizeRequest(
at.torch.compose.nostr.Relays.DefaultDMRelayList.shuffled().map { normalizedRelayUrl -> at.torch.compose.nostr.Relays.DefaultDMRelayList.shuffled().map { normalizedRelayUrl ->
_root_ide_package_.at.torch.compose.database.model.NegentropySynchronizeRequest( NegentropySynchronizeRequest(
id = at.torch.compose.database.model.NegentropySynchronizeRequest.computeId( id = NegentropySynchronizeRequest.computeId(
relayURL = normalizedRelayUrl.url, relayURL = normalizedRelayUrl.url,
synchronizationFilter = synchronizationFilter synchronizationFilter = chatRequestFilter
), ),
purpose = "chat", purpose = "chat",
synchronizationFilter = synchronizationFilter, synchronizationFilter = chatRequestFilter,
relayURL = normalizedRelayUrl.url, relayURL = normalizedRelayUrl.url,
level = 0 level = 0
) )
} }
) )
// Get/observe groupIds for publicKey
val chatRooms = chatRepository.getChatRoomListByPublicKey(publicKey)
val relayChatRoomMaps = Relays.DefaultDMRelayList.map { dmRelay ->
dmRelay.url to chatRooms.map { localChatRoom ->
// TODO: Just return localChatRoom.chatRoom.id once we have nostrGroupId stored as the chatRoom primary id
if (localChatRoom.chatRoom.mlsGroupState != null) {
if (localChatRoom.chatRoom.id.length == 64) {
localChatRoom.chatRoom.id
} else {
val group = MlsGroup.restore(
MlsGroupState.decodeTls(
localChatRoom.chatRoom.mlsGroupState.hexToByteArray()
)
)
logger.d("chatRoomId: ${localChatRoom.chatRoom.id} vs nostrGroupId (${group.currentMarmotData()?.nostrGroupId})")
group.currentMarmotData()?.nostrGroupId ?: localChatRoom.chatRoom.id
}
} else {
localChatRoom.chatRoom.id
}
}
}
// TODO: Get relay with chatGroups...
val negentropySyncRequests = relayChatRoomMaps.map { relayChatRoomMap ->
val mlsGroupMessageFilter = SynchronizationFilter(
kinds = arrayOf(GroupEvent.KIND),
tags = mapOf("h" to relayChatRoomMap.second),
)
logger.d("mlsGroupMessageFilter: $mlsGroupMessageFilter")
NegentropySynchronizeRequest(
id = NegentropySynchronizeRequest.computeId(
relayURL = relayChatRoomMap.first,
synchronizationFilter = mlsGroupMessageFilter
),
purpose = "messages",
synchronizationFilter = mlsGroupMessageFilter,
relayURL = relayChatRoomMap.first,
level = 0
)
}
logger.d("negentropySyncRequests: ${negentropySyncRequests.map { it.synchronizationFilter }}")
nostrRepository.queueNegentropySynchronizeRequest(
negentropySyncRequests
)
} }
} }
@@ -178,7 +244,6 @@ class ChatRoomListViewModel(
fun factory( fun factory(
initialChatRoomListUIState: at.torch.compose.ui.view.state.ChatRoomListUIState = at.torch.compose.ui.view.state.ChatRoomListUIState.Loading, initialChatRoomListUIState: at.torch.compose.ui.view.state.ChatRoomListUIState = at.torch.compose.ui.view.state.ChatRoomListUIState.Loading,
publicKey: String, publicKey: String,
synchronizationFilter: at.torch.compose.database.model.types.SynchronizationFilter,
nostrRepository: at.torch.compose.repository.NostrRepository, nostrRepository: at.torch.compose.repository.NostrRepository,
chatRepository: at.torch.compose.repository.ChatRepository chatRepository: at.torch.compose.repository.ChatRepository
): ViewModelProvider.Factory = viewModelFactory { ): ViewModelProvider.Factory = viewModelFactory {
@@ -186,7 +251,6 @@ class ChatRoomListViewModel(
ChatRoomListViewModel( ChatRoomListViewModel(
initialChatRoomListUIState = initialChatRoomListUIState, initialChatRoomListUIState = initialChatRoomListUIState,
publicKey = publicKey, publicKey = publicKey,
synchronizationFilter = synchronizationFilter,
nostrRepository = nostrRepository, nostrRepository = nostrRepository,
chatRepository = chatRepository chatRepository = chatRepository
) )
@@ -9,7 +9,9 @@ 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.types.SynchronizationFilter
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -56,29 +58,6 @@ class HomeViewModel(
} }
} }
fun getSynchronizationFilter(
homeScreenType: HomeScreenType,
profileWithFollowing: at.torch.compose.database.model.intermdiate.LocalProfileWithFollowing,
): at.torch.compose.database.model.types.SynchronizationFilter {
Clock.System.now().minus(12.hours)
Clock.System.now()
return when (homeScreenType) {
HomeScreenType.Messages -> {
_root_ide_package_.at.torch.compose.database.model.types.SynchronizationFilter(
kinds = arrayOf(
GiftWrapEvent.KIND,
),
tags = mapOf(
Pair("p", listOf(activeUserPublicKey))
),
limit = 50
)
}
}
}
companion object { companion object {
const val TAG = "HomeViewModel" const val TAG = "HomeViewModel"
@@ -75,18 +75,18 @@ class NavigationViewModel(
} }
} }
} else { } else {
if (startupRoute != null) { // if (startupRoute != null) {
val localAccount = nostrRepository.observeProfile( // val localAccount = nostrRepository.observeProfile(
publicKey = activeUserPublicKey // publicKey = activeUserPublicKey
).firstOrNull() // ).firstOrNull()
//
processLocalAccount(localAccount) // processLocalAccount(localAccount)
//
} else { // } else {
_navigationUIState.getAndUpdate { // _navigationUIState.getAndUpdate {
NavigationUIState.StartupPhoenix // NavigationUIState.StartupPhoenix
} // }
} // }
} }
} }