Working with ChatMessageRelayList

This commit is contained in:
Kgothatso Ngako
2026-06-04 12:31:25 +02:00
parent efbbb729fc
commit 7c10630188
11 changed files with 190 additions and 32 deletions

View File

@@ -18,5 +18,5 @@ interface BroadcastNostrEventReceiptDao {
fun getBroadcastNostrEventReceiptByNostrEventId(nostrEventId: String): Flow<BroadcastNostrEventReceipt?>
@Upsert
suspend fun upsert(broadcastNostrEventReceipt: BroadcastNostrEventReceipt)
suspend fun upsert(broadcastNostrEventReceipt: BroadcastNostrEventReceipt): Long
}

View File

@@ -1,11 +1,19 @@
package ac.cord.auxiliary.compose.database.dao
import ac.cord.auxiliary.compose.database.model.ChatMessageBroadcastNostrEventRequestRelation
import ac.cord.auxiliary.compose.database.model.NostrEvent
import androidx.room3.Dao
import androidx.room3.Query
import androidx.room3.Transaction
import androidx.room3.Upsert
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind
import kotlin.time.Instant
@Dao
interface ChatMessageBroadcastNostrEventRequestRelationDao {
@Query("SELECT * FROM ChatMessageBroadcastNostrEventRequestRelation WHERE broadcastNostrEventRequestId = :broadcastNostrEventRequestId")
suspend fun getChatMessageBroadcastNostrEventRequestRelationByBroadcastRequest(broadcastNostrEventRequestId: Long): ChatMessageBroadcastNostrEventRequestRelation?
@Upsert
suspend fun upsert(chatMessageBroadcastNostrEventRequestRelation: ChatMessageBroadcastNostrEventRequestRelation)

View File

@@ -371,12 +371,13 @@ abstract class NostrDao(
)
if (localChatRoom == null) {
val userPublicKey = SeedManager.activeKeyPair().pubKey.toHex()
database.chatRoomDao().upsert(
ChatRoom(
id = chatRoomId,
userPublicKey = SeedManager.activeKeyPair().pubKey.toHex(),
subject = null, // TODO: Get subject from tags...
createdAt = giftWrapPayload.createdAt,
userPublicKey = userPublicKey,
subject = decryptedGiftWrapPayload.parseSubject(), // TODO: Get subject from tags...
createdAt = decryptedGiftWrapPayload.createdAt,
initialGiftWrapPayloadId = giftWrapPayload.id
)
)
@@ -425,6 +426,71 @@ abstract class NostrDao(
database.participantDao().upsert(
participants
)
participants.filter { it.participantPublicKey != userPublicKey }.forEach { participant ->
val publicKey = participant.participantPublicKey
val chatMessageRelayListEvent = database.nostrEventDao().getAuthoredNostrEvents(
kinds = arrayOf(
ChatMessageRelayListEvent.KIND
),
authors = arrayOf(
publicKey
),
since = GENESIS_AT,
limit = 5
).firstOrNull()?.let { nostrEvent ->
if (nostrEvent.kind != ChatMessageRelayListEvent.KIND) {
logger.e("getAuthoredNostrEvents returned invalid ChatMessageRelayListEvent for ${publicKey}: $nostrEvent")
null
} else {
ChatMessageRelayListEvent(
id = nostrEvent.id,
tags = nostrEvent.tags,
pubKey = nostrEvent.pubKey,
content = nostrEvent.content,
createdAt = nostrEvent.createdAt.epochSeconds,
sig = nostrEvent.sig
)
}
}
if (chatMessageRelayListEvent != null) {
// Sync messages from this relay...
val synchronizationFilter = SynchronizationFilter(
kinds = arrayOf(
GiftWrapEvent.KIND,
),
tags = mapOf(
Pair("p", listOf(participant.participantPublicKey))
),
limit = 50
)
database.negentropySynchronizeRequestDao().insert(
chatMessageRelayListEvent.relays().map { normalizedRelayUrl ->
NegentropySynchronizeRequest(
id = NegentropySynchronizeRequest.computeId(
relayURL = normalizedRelayUrl.url,
synchronizationFilter = synchronizationFilter
),
purpose = "sent-messages",
synchronizationFilter = synchronizationFilter,
relayURL = normalizedRelayUrl.url,
level = 0
)
}
)
} else {
logger.w("We don't have a chatMessageRelayListEvent for the pubkey $publicKey")
// Sync ChatMessageRelayListEvent publicKey...
// TODO: Get relayHint form participant...
if (profilePublicKeysToSync.containsKey(relayURL)) {
profilePublicKeysToSync[relayURL] = mutableSetOf()
}
profilePublicKeysToSync[relayURL]?.add(participant.participantPublicKey)
}
}
} else {
giftWrapPayload.parseSubject()?.let { subject ->
database.chatRoomDao().upsert(

View File

@@ -44,6 +44,8 @@ data class Participant(
val participantPublicKey: HexKey,
val chatRoomId: String,
// TODO: Add relayHint...
override val createdAt: Instant = Clock.System.now(),
override val updatedAt: Instant = Clock.System.now(),
override val savedAt: Instant = Clock.System.now(),

View File

@@ -1,6 +1,7 @@
package ac.cord.auxiliary.compose.database.repository
import ac.cord.auxiliary.compose.database.AuxDatabase
import ac.cord.auxiliary.compose.database.GENESIS_AT
import ac.cord.auxiliary.compose.database.model.ChatMessage
import ac.cord.auxiliary.compose.database.model.ChatMessageNostrEventRelation
import ac.cord.auxiliary.compose.database.model.ChatRoom
@@ -15,6 +16,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -54,6 +56,34 @@ class DatabaseChatRepository(
return null
}
override suspend fun getChatMessageRelayForPublicKey(publicKey: HexKey): ChatMessageRelayListEvent? {
// TODO: Implement a UserChatMessageRelayList model
return database.nostrEventDao().getAuthoredNostrEvents(
kinds = arrayOf(
ChatMessageRelayListEvent.KIND
),
authors = arrayOf(
publicKey
),
since = GENESIS_AT,
limit = 5
).firstOrNull()?.let { nostrEvent ->
if (nostrEvent.kind != ChatMessageRelayListEvent.KIND) {
logger.e("getAuthoredNostrEvents returned invalid ChatMessageRelayListEvent for ${publicKey}: $nostrEvent")
null
} else {
ChatMessageRelayListEvent(
id = nostrEvent.id,
tags = nostrEvent.tags,
pubKey = nostrEvent.pubKey,
content = nostrEvent.content,
createdAt = nostrEvent.createdAt.epochSeconds,
sig = nostrEvent.sig
)
}
}
}
override suspend fun sendChatMessage(
text: String,
localChatRoom: LocalChatRoom,

View File

@@ -4,6 +4,7 @@ import ac.cord.auxiliary.compose.database.AuxDatabase
import ac.cord.auxiliary.compose.database.GENESIS_AT
import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventReceipt
import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventRequest
import ac.cord.auxiliary.compose.database.model.ChatMessageBroadcastNostrEventReceiptRelation
import ac.cord.auxiliary.compose.database.model.Connection
import ac.cord.auxiliary.compose.database.model.NegentropySynchronizeRequest
import ac.cord.auxiliary.compose.database.model.NostrEvent
@@ -406,6 +407,29 @@ class DatabaseNostrRepository(
status = status
)
)
if (status == "published") {
val broadcastNostrEventReceiptId = database.broadcastNostrEventReceiptDao().upsert(
BroadcastNostrEventReceipt(
nostrEventId = broadcastNostrEventRequest.nostrEventId,
unsignedNostrEventId = broadcastNostrEventRequest.unsignedNostrEventId,
isAccepted = true,
relayURL = broadcastNostrEventRequest.relayURL
)
)
database.chatMessageBroadcastNostrEventRequestRelationDao().getChatMessageBroadcastNostrEventRequestRelationByBroadcastRequest(
broadcastNostrEventRequestId = broadcastNostrEventRequest.id
)?.let { chatMessageBroadcastNostrEventRequestRelation ->
// This is actually a chatMessage related broadcast event...
database.chatMessageBroadcastNostrEventReceiptRelationDao().upsert(
ChatMessageBroadcastNostrEventReceiptRelation(
chatMessageId = chatMessageBroadcastNostrEventRequestRelation.chatMessageId,
broadcastNostrEventReceiptId = broadcastNostrEventReceiptId
)
)
}
}
}
override suspend fun synchronizeNostrEventRequestProcessed(synchronizeNostrEventRequest: SynchronizeNostrEventRequest) {

View File

@@ -9,6 +9,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@@ -23,6 +24,8 @@ interface ChatRepository {
suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String? = null): LocalChatRoom?
suspend fun getChatMessageRelayForPublicKey(publicKey: HexKey): ChatMessageRelayListEvent?
suspend fun sendChatMessage(
text: String,
localChatRoom: LocalChatRoom,
@@ -58,6 +61,10 @@ interface ChatRepository {
return null
}
override suspend fun getChatMessageRelayForPublicKey(publicKey: HexKey): ChatMessageRelayListEvent? {
return null
}
override suspend fun sendChatMessage(
text: String,
localChatRoom: LocalChatRoom,

View File

@@ -118,6 +118,7 @@ fun AuxNavHost(
val synchronizationViewModel: SynchronizationViewModel = viewModel(
factory = SynchronizationViewModel.factory(
nostrRepository = databaseNostrRepository,
chatRepository = databaseChatRepository,
relayRepository = databaseNostrRepository,
scope = applicationIOScope
)

View File

@@ -1,6 +1,8 @@
package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.database.model.NegentropySynchronizeRequest
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
import ac.cord.auxiliary.compose.extensions.toFormattedTimeAndDateString
import ac.cord.auxiliary.compose.repository.ChatRepository
import ac.cord.auxiliary.compose.repository.NostrRepository
@@ -24,9 +26,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccessTime
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.KeyOff
import androidx.compose.material.icons.filled.LockClock
import androidx.compose.material.icons.filled.Pending
import androidx.compose.material.icons.filled.PendingActions
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
@@ -34,6 +34,7 @@ import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -48,6 +49,7 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -63,6 +65,8 @@ class ChatRoomDetailMessageListViewModel(
var chatRoomDetailMessageListUIState: ChatRoomDetailMessageListUIState by mutableStateOf(initialChatRoomDetailMessageListUIState)
private set
val isReceiverChatMessageRelayListMissing: MutableState<Boolean> = mutableStateOf(false)
fun initiate() {
logger.d("init")
scheduleSynchronization()
@@ -85,23 +89,43 @@ class ChatRoomDetailMessageListViewModel(
fun scheduleSynchronization() {
logger.d("scheduleSynchronization")
// viewModelScope.launch(Dispatchers.IO) {
// // Sync Notifications... might want to also run this in the background
// nostrRepository.queueNegentropySynchronizeRequest(
// Relays.DefaultDMRelayList.shuffled().map { normalizedRelayUrl ->
// NegentropySynchronizeRequest(
// id = NegentropySynchronizeRequest.computeId(
// relayURL = normalizedRelayUrl.url,
// synchronizationFilter = synchronizationFilter
// ),
// purpose = "chat",
// synchronizationFilter = synchronizationFilter,
// relayURL = normalizedRelayUrl.url,
// level = 0
// )
// }
// )
// }
viewModelScope.launch(Dispatchers.IO) {
// Sync Notifications... might want to also run this in the background
localChatRoom.participants.filter { it.participant.participantPublicKey != localChatRoom.chatRoom.userPublicKey }.forEach { recipients ->
val chatMessageRelayListEvent = chatRepository.getChatMessageRelayForPublicKey(recipients.participant.participantPublicKey)
if (chatMessageRelayListEvent != null) {
// Sync messages from this relay...
val synchronizationFilter = SynchronizationFilter(
kinds = arrayOf(
GiftWrapEvent.KIND,
),
tags = mapOf(
Pair("p", listOf(recipients.participant.participantPublicKey))
),
limit = 50
)
nostrRepository.queueNegentropySynchronizeRequest(
chatMessageRelayListEvent.relays().map { normalizedRelayUrl ->
NegentropySynchronizeRequest(
id = NegentropySynchronizeRequest.computeId(
relayURL = normalizedRelayUrl.url,
synchronizationFilter = synchronizationFilter
),
purpose = "sent-messages",
synchronizationFilter = synchronizationFilter,
relayURL = normalizedRelayUrl.url,
level = 0
)
}
)
} else {
isReceiverChatMessageRelayListMissing.value = true
}
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -218,7 +242,6 @@ class ChatRoomDetailMessageListViewModel(
style = MaterialTheme.typography.labelSmall
)
if (localChatMessage.chatMessage.isUserMessage) {
// TODO: Add delivered and read receipt..
if (localChatMessage.chatMessageBroadcastNostrEventReceiptRelation != null) {
Icon(
Icons.Default.Check,

View File

@@ -34,13 +34,7 @@ class ChatRoomDetailViewModel(
val isActionPending: MutableState<Boolean> = mutableStateOf(false)
fun queueSynchronization() {
logger.d { "queueSynchronization" }
}
// TODO: Render MessageList...
fun initiateChatRoomDetail() {
// TODO: GetOrCreateChatRoom
if (Crypto.isPubKeyCompressed(directMessageId.hexToByteArray())) {
logger.d("compressed: $directMessageId")
// Get ChatRoomById
@@ -71,7 +65,6 @@ class ChatRoomDetailViewModel(
}
}
}
queueSynchronization()
}

View File

@@ -9,6 +9,7 @@ import ac.cord.auxiliary.compose.network.relays.RelaysSocketManager
import ac.cord.auxiliary.compose.network.sockets.NostrIncomingMessage
import ac.cord.auxiliary.compose.network.sockets.NostrSocketClientFactory
import ac.cord.auxiliary.compose.repository.CachingImportRepository
import ac.cord.auxiliary.compose.repository.ChatRepository
import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.repository.RelayRepository
import androidx.lifecycle.ViewModel
@@ -37,6 +38,7 @@ import kotlin.time.Duration.Companion.milliseconds
class SynchronizationViewModel(
val nostrRepository: NostrRepository,
val chatRepository: ChatRepository,
val relayRepository: RelayRepository,
val scope: CoroutineScope,
): ViewModel() {
@@ -54,12 +56,14 @@ class SynchronizationViewModel(
fun factory(
nostrRepository: NostrRepository,
chatRepository: ChatRepository,
relayRepository: RelayRepository,
scope: CoroutineScope,
): ViewModelProvider.Factory = viewModelFactory {
initializer {
SynchronizationViewModel(
nostrRepository = nostrRepository,
chatRepository = chatRepository,
relayRepository = relayRepository,
scope = scope
)
@@ -372,7 +376,7 @@ class SynchronizationViewModel(
logger.d("Nostr Publish Result: $nostrPublishResult")
nostrRepository.broadcastProcessed(
localBroadcastNostrEventRequest.broadcastNostrEventRequest,
"published"
"published" // TODO: Might want to pass the nostrPublishResult.result
)
}
}