Sync MLS group messages

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

View File

@@ -20,6 +20,7 @@ import at.torch.compose.exceptions.GiftWrapImpersonationException
import at.torch.compose.exceptions.GiftWrapSealDecryptionException
import at.torch.compose.exceptions.GiftWrapUnsealException
import at.torch.compose.exceptions.MarmotMissingKeyPackageBundleException
import at.torch.compose.exceptions.MarmotMissingNostrGroupDataExtension
import at.torch.compose.exceptions.MarmotWelcomeEventMissingKeyPackageEventIdException
import at.torch.compose.extensions.toHex
import at.torch.compose.network.serialization.encodeToJsonString
@@ -615,7 +616,7 @@ abstract class NostrDao(
)
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")

View File

@@ -78,8 +78,8 @@ data class ChatRoom(
): at.torch.compose.database.model.traits.TimestampedEntity,
at.torch.compose.database.model.traits.LocalStoreEntity,
at.torch.compose.database.model.traits.UserViewableEntity,
at.torch.compose.database.model.traits.SoftDeletableEntity {
at.torch.compose.database.model.traits.SoftDeletableEntity
{
companion object {
const val TAG = "ChatRoom"

View File

@@ -185,10 +185,6 @@ fun HomeScreen(
factory = at.torch.compose.ui.view.model.ChatRoomListViewModel.factory(
initialChatRoomListUIState = at.torch.compose.ui.view.state.ChatRoomListUIState.Loading,
publicKey = homeScreenUIState.profileWithFollowing.profile.publicKey,
synchronizationFilter = homeScreenViewModel.getSynchronizationFilter(
homeScreenTypeType,
profileWithFollowing = homeScreenUIState.profileWithFollowing
),
nostrRepository = nostrRepository,
chatRepository = chatRepository
),

View File

@@ -164,7 +164,7 @@ fun ShareProfileScreen(
clipboardManager.setText(
buildAnnotatedString {
activeUserPublicKey.hexToNpubHrp()
append(activeUserPublicKey.hexToNpubHrp())
}
)
}

View File

@@ -25,15 +25,23 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
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 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.IO
import kotlinx.coroutines.launch
import kotlin.math.log
class ChatRoomListViewModel(
initialChatRoomListUIState: at.torch.compose.ui.view.state.ChatRoomListUIState,
val publicKey: String,
val synchronizationFilter: at.torch.compose.database.model.types.SynchronizationFilter,
val nostrRepository: at.torch.compose.repository.NostrRepository,
val chatRepository: at.torch.compose.repository.ChatRepository
): ViewModel() {
@@ -65,20 +73,78 @@ class ChatRoomListViewModel(
logger.d("scheduleSynchronization")
viewModelScope.launch(Dispatchers.IO) {
// 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(
at.torch.compose.nostr.Relays.DefaultDMRelayList.shuffled().map { normalizedRelayUrl ->
_root_ide_package_.at.torch.compose.database.model.NegentropySynchronizeRequest(
id = at.torch.compose.database.model.NegentropySynchronizeRequest.computeId(
NegentropySynchronizeRequest(
id = NegentropySynchronizeRequest.computeId(
relayURL = normalizedRelayUrl.url,
synchronizationFilter = synchronizationFilter
synchronizationFilter = chatRequestFilter
),
purpose = "chat",
synchronizationFilter = synchronizationFilter,
synchronizationFilter = chatRequestFilter,
relayURL = normalizedRelayUrl.url,
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(
initialChatRoomListUIState: at.torch.compose.ui.view.state.ChatRoomListUIState = at.torch.compose.ui.view.state.ChatRoomListUIState.Loading,
publicKey: String,
synchronizationFilter: at.torch.compose.database.model.types.SynchronizationFilter,
nostrRepository: at.torch.compose.repository.NostrRepository,
chatRepository: at.torch.compose.repository.ChatRepository
): ViewModelProvider.Factory = viewModelFactory {
@@ -186,7 +251,6 @@ class ChatRoomListViewModel(
ChatRoomListViewModel(
initialChatRoomListUIState = initialChatRoomListUIState,
publicKey = publicKey,
synchronizationFilter = synchronizationFilter,
nostrRepository = nostrRepository,
chatRepository = chatRepository
)

View File

@@ -9,7 +9,9 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import at.torch.compose.database.model.types.SynchronizationFilter
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
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 {
const val TAG = "HomeViewModel"

View File

@@ -75,18 +75,18 @@ class NavigationViewModel(
}
}
} else {
if (startupRoute != null) {
val localAccount = nostrRepository.observeProfile(
publicKey = activeUserPublicKey
).firstOrNull()
processLocalAccount(localAccount)
} else {
_navigationUIState.getAndUpdate {
NavigationUIState.StartupPhoenix
}
}
// if (startupRoute != null) {
// val localAccount = nostrRepository.observeProfile(
// publicKey = activeUserPublicKey
// ).firstOrNull()
//
// processLocalAccount(localAccount)
//
// } else {
// _navigationUIState.getAndUpdate {
// NavigationUIState.StartupPhoenix
// }
// }
}
}