Relay hints for chat participants

This commit is contained in:
Kgothatso Ngako
2026-06-04 14:53:58 +02:00
parent 0d4b517506
commit d140d1a917
16 changed files with 92 additions and 46 deletions

View File

@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "a7a5014361138681b4a2123531f2a0e3",
"identityHash": "7aff70ac118cd5856e8a4939d62a2ed8",
"entities": [
{
"tableName": "BroadcastNostrEventReceipt",
@@ -1357,7 +1357,7 @@
},
{
"tableName": "NostrEvent",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `pubKey` TEXT NOT NULL, `kind` INTEGER NOT NULL, `tags` TEXT NOT NULL, `content` TEXT NOT NULL, `sig` TEXT NOT NULL, `quotedNostrEventId` TEXT, `quotedAuthorPublicKey` TEXT, `inReplyToNostrEventId` TEXT, `inReplyToAuthorPublicKey` TEXT, `inReplyToRootNostrEventId` TEXT, `inReplyToRootAuthorPublicKey` TEXT, `repostedNostrEventId` TEXT, `repostedAuthorPublicKey` TEXT, `unsignedNostrEventId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, PRIMARY KEY(`id`))",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `pubKey` TEXT NOT NULL, `kind` INTEGER NOT NULL, `tags` TEXT NOT NULL, `content` TEXT NOT NULL, `sig` TEXT NOT NULL, `relayUrl` TEXT, `quotedNostrEventId` TEXT, `quotedAuthorPublicKey` TEXT, `inReplyToNostrEventId` TEXT, `inReplyToAuthorPublicKey` TEXT, `inReplyToRootNostrEventId` TEXT, `inReplyToRootAuthorPublicKey` TEXT, `repostedNostrEventId` TEXT, `repostedAuthorPublicKey` TEXT, `unsignedNostrEventId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
@@ -1395,6 +1395,11 @@
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "relayUrl",
"columnName": "relayUrl",
"affinity": "TEXT"
},
{
"fieldPath": "quotedNostrEventId",
"columnName": "quotedNostrEventId",
@@ -1586,7 +1591,7 @@
},
{
"tableName": "Participant",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `participantPublicKey` TEXT NOT NULL, `chatRoomId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, FOREIGN KEY(`participantPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`chatRoomId`) REFERENCES `ChatRoom`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `participantPublicKey` TEXT NOT NULL, `chatRoomId` TEXT NOT NULL, `relayHint` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, FOREIGN KEY(`participantPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`chatRoomId`) REFERENCES `ChatRoom`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
@@ -1606,6 +1611,11 @@
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "relayHint",
"columnName": "relayHint",
"affinity": "TEXT"
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
@@ -2847,7 +2857,7 @@
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a7a5014361138681b4a2123531f2a0e3')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7aff70ac118cd5856e8a4939d62a2ed8')"
]
}
}

View File

@@ -21,6 +21,7 @@ import androidx.room3.Dao
import androidx.room3.Transaction
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
@@ -376,17 +377,20 @@ abstract class NostrDao(
ChatRoom(
id = chatRoomId,
userPublicKey = userPublicKey,
subject = decryptedGiftWrapPayload.parseSubject(), // TODO: Get subject from tags...
subject = decryptedGiftWrapPayload.parseSubject(),
createdAt = decryptedGiftWrapPayload.createdAt,
initialGiftWrapPayloadId = giftWrapPayload.id
)
)
// Add participants...
val participants = giftWrapPayload.participantPubKeys().map {
val participants = giftWrapPayload.participantPTags(
NormalizedRelayUrl(relayURL) // TODO: Get relayURL for publicKey profile...
).map {
Participant(
participantPublicKey = it,
chatRoomId = chatRoomId
participantPublicKey = it.pubKey,
chatRoomId = chatRoomId,
relayHint = it.relayHint?.url
)
}

View File

@@ -32,7 +32,7 @@ abstract class NostrNip17Dao(
val logger = Logger.withTag(TAG)
@Transaction
open suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String? = null): LocalChatRoom? {
open suspend fun getOrCreateChatRoom(publicKey: String, relayHint: String?, defaultSubject: String? = null): LocalChatRoom? {
logger.d("getOrCreateChatRoom: $publicKey")
val activePublicKey = SeedManager.activePublicKey().toHexKey()
@@ -64,7 +64,8 @@ abstract class NostrNip17Dao(
profiles.map {
Participant(
participantPublicKey = it.publicKey,
chatRoomId = chatRoomId
chatRoomId = chatRoomId,
relayHint = relayHint
)
}
)

View File

@@ -11,11 +11,10 @@ import androidx.room3.PrimaryKey
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
import com.vitorpamplona.quartz.nip14Subject.subject
import fr.acinq.bitcoin.PublicKey
import fr.acinq.bitcoin.crypto.musig2.Musig2
import kotlin.time.Clock
import kotlin.time.Instant
@@ -84,27 +83,33 @@ data class GiftWrapPayload( // TODO: Rename this to GiftWrapPayload...
fun aggregatedParticipantsPublicKey(): String? = try {
logger.d("aggregateParticipantsPublicKeys")
val hexKeySet = participantPubKeys()
val hexKeySet = participantPTags()
return ChatRoom.deriveChatRoomId(hexKeySet)
return ChatRoom.deriveChatRoomId(
hexKeySet.map { it.pubKey }.toSet()
)
} catch (e: Throwable) {
logger.e("Failed to aggregate participants publicKeys", e)
return null
}
fun participantPubKeys(): Set<HexKey> = try {
val hexKeys = mutableSetOf(
publicKey
fun participantPTags(relayUrl: NormalizedRelayUrl? = null): Set<PTag> = try {
val pTags = mutableSetOf(
PTag(
pubKey = publicKey,
relayHint = relayUrl
)
)
hexKeys.addAll(
pTags.addAll(
tags.taggedUsers().map {
it.pubKey
it
}
)
return hexKeys
return pTags
} catch (e: Throwable) {
logger.e("Failed to get participant public keys", e)
logger.e("Failed to get pTags", e)
return emptySet()
}

View File

@@ -56,6 +56,11 @@ data class NostrEvent(
val content: String,
val sig: HexKey,
/**
* Knew which relay this nostr event came from and use that as a relayHint if need be...
*/
val relayUrl: String? = null,
val quotedNostrEventId: HexKey? = null,
val quotedAuthorPublicKey: HexKey? = null,

View File

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

View File

@@ -1,10 +1,7 @@
package ac.cord.auxiliary.compose.database.model.intermdiate
import ac.cord.auxiliary.compose.database.model.ChatRoom
import ac.cord.auxiliary.compose.database.model.Mention
import ac.cord.auxiliary.compose.database.model.Participant
import ac.cord.auxiliary.compose.database.model.Post
import ac.cord.auxiliary.compose.database.model.Profile
import ac.cord.auxiliary.compose.ui.composable.widgets.profile.ProfileColor
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -22,7 +19,7 @@ data class LocalChatRoom(
parentColumns = ["id"],
entityColumns = ["chatRoomId"],
)
val participants: List<LocalParticipant> = emptyList(),
val localParticipants: List<LocalParticipant> = emptyList(),
) {
@Composable
fun RenderChatRoomTitleText() {
@@ -32,13 +29,13 @@ data class LocalChatRoom(
color = ProfileColor.fromPublicKey(chatRoom.id)
)
} else {
if (participants.size == 1) {
if (localParticipants.size == 1) {
Text(
text = "Note to Self (${participants.first().profile?.humanReadableNameOrPubkey()})"
text = "Note to Self (${localParticipants.first().profile?.humanReadableNameOrPubkey()})"
)
} else {
// Remove the active user publicKey from participants...
val participantNames = participants.filter { it.participant.participantPublicKey != chatRoom.userPublicKey }.map { buildAnnotatedString {
val participantNames = localParticipants.filter { it.participant.participantPublicKey != chatRoom.userPublicKey }.map { buildAnnotatedString {
withStyle(style = SpanStyle(color = ProfileColor.fromPublicKey(it.participant.participantPublicKey))) {
append(it.profile?.humanReadableNameOrPubkey() ?: "unknown participant")
}

View File

@@ -3,7 +3,6 @@ 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
import ac.cord.auxiliary.compose.database.model.GiftWrapPayload
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatMessage
@@ -13,6 +12,7 @@ import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
@@ -46,9 +46,14 @@ class DatabaseChatRepository(
}
override suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String?): LocalChatRoom? = try {
override suspend fun getOrCreateChatRoom(
publicKey: String,
relayHint: String?,
defaultSubject: String?
): LocalChatRoom? = try {
return database.nostrNip17Dao().getOrCreateChatRoom(
publicKey,
relayHint = relayHint,
defaultSubject = defaultSubject
)
} catch (e: Throwable) {
@@ -91,16 +96,19 @@ class DatabaseChatRepository(
) {
logger.i("sendChatMessage($text): $localChatRoom")
val receiverTags = localChatRoom.participants.filter { participant ->
participant.participant.participantPublicKey != localChatRoom.chatRoom.userPublicKey
}.map { participant ->
PTag.assemble(participant.participant.participantPublicKey, null) // TODO: Get the relays associated with the participant...
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 giftWrapPayloadId = database.giftWrapPayloadDao().upsert(
GiftWrapPayload(
kind = messageType,
tags = receiverTags.toTypedArray(), // TODO: Get participants...
tags = receiverTags.toTypedArray(),
content = text,
publicKey = localChatRoom.chatRoom.userPublicKey
)

View File

@@ -1,6 +1,5 @@
package ac.cord.auxiliary.compose.repository
import ac.cord.auxiliary.compose.database.model.ChatMessage
import ac.cord.auxiliary.compose.database.model.ChatRoom
import ac.cord.auxiliary.compose.database.model.GiftWrapPayload
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatMessage
@@ -22,7 +21,7 @@ interface ChatRepository {
suspend fun observeChatMessageListByChatRoomId(chatRoomId: String): Flow<List<LocalChatMessage>>
suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String? = null): LocalChatRoom?
suspend fun getOrCreateChatRoom(publicKey: String, relayHint: String?, defaultSubject: String? = null): LocalChatRoom?
suspend fun getChatMessageRelayForPublicKey(publicKey: HexKey): ChatMessageRelayListEvent?
@@ -57,7 +56,11 @@ interface ChatRepository {
}
}
override suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String?): LocalChatRoom? {
override suspend fun getOrCreateChatRoom(
publicKey: String,
relayHint: String?,
defaultSubject: String?
): LocalChatRoom? {
return null
}

View File

@@ -50,6 +50,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun ChatRoomDetailScreen(
publicKey: String,
relayHint: String?,
initialChatRoomDetailUIState: ChatRoomDetailUIState = ChatRoomDetailUIState.Loading,
nostrRepository: NostrRepository,
chatRepository: ChatRepository
@@ -57,6 +58,7 @@ fun ChatRoomDetailScreen(
val chatRoomDetailViewModel: ChatRoomDetailViewModel = viewModel(
factory = ChatRoomDetailViewModel.factory(
publicKey = publicKey,
relayHint = relayHint,
initialChatRoomDetailUIState = initialChatRoomDetailUIState,
nostrRepository = nostrRepository,
chatRepository = chatRepository
@@ -269,6 +271,7 @@ private fun ChatRoomDetailScreenPreview() {
) {
ChatRoomDetailScreen(
publicKey = "publicKey",
relayHint = null,
initialChatRoomDetailUIState = ChatRoomDetailUIState.Loaded(
localChatRoom = LocalChatRoom(
chatRoom = ChatRoom(

View File

@@ -376,6 +376,7 @@ fun AuxNavHost(
ChatRoomDetailScreen(
publicKey = route.directMessageIdentifier,
relayHint = route.relayHint,
nostrRepository = databaseNostrRepository,
chatRepository = databaseChatRepository
)

View File

@@ -5,5 +5,6 @@ import kotlinx.serialization.Serializable
@Serializable
data class ChatRoomDetailRoute(
val directMessageIdentifier: String, // TODO: have this as a publicKey
val relayHint: String?
): Route() {
}

View File

@@ -227,7 +227,8 @@ fun MetadataEventDetail(
onClick = {
onNavigateToChatRoom.invoke(
ChatRoomDetailRoute(
directMessageIdentifier = localNostrEvent.nostrEvent.pubKey
directMessageIdentifier = localNostrEvent.nostrEvent.pubKey,
relayHint = localNostrEvent.nostrEvent.relayUrl,
)
)
}

View File

@@ -96,7 +96,7 @@ class ChatRoomDetailMessageListViewModel(
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 ->
localChatRoom.localParticipants.filter { it.participant.participantPublicKey != localChatRoom.chatRoom.userPublicKey }.forEach { recipients ->
val chatMessageRelayListEvent = chatRepository.getChatMessageRelayForPublicKey(recipients.participant.participantPublicKey)

View File

@@ -20,6 +20,7 @@ import kotlinx.coroutines.launch
class ChatRoomDetailViewModel(
val directMessageId: String,
val relayHint: String?,
initialChatRoomDetailUIState: ChatRoomDetailUIState,
val nostrRepository: NostrRepository,
val chatRepository: ChatRepository
@@ -36,7 +37,7 @@ class ChatRoomDetailViewModel(
fun initiateChatRoomDetail() {
if (Crypto.isPubKeyCompressed(directMessageId.hexToByteArray())) {
logger.d("compressed: $directMessageId")
logger.d("compressed (most likely chat room): $directMessageId")
// Get ChatRoomById
viewModelScope.launch(Dispatchers.IO) {
val localChatRoom = chatRepository.getChatRoomByIdentifier(directMessageId)
@@ -51,10 +52,13 @@ class ChatRoomDetailViewModel(
}
} else {
logger.d("Not compressed key publicKey: $directMessageId")
logger.d("Uncompressed publicKey (most likely npub): $directMessageId")
viewModelScope.launch(Dispatchers.IO) {
// We need to get or create a chat with the user on this publicKey...
val localChatRoom = chatRepository.getOrCreateChatRoom(directMessageId)
val localChatRoom = chatRepository.getOrCreateChatRoom(
publicKey = directMessageId,
relayHint = relayHint
)
chatRoomDetailUIState = if (localChatRoom == null) {
ChatRoomDetailUIState.Error
@@ -73,6 +77,7 @@ class ChatRoomDetailViewModel(
fun factory(
publicKey: String,
relayHint: String?,
initialChatRoomDetailUIState: ChatRoomDetailUIState = ChatRoomDetailUIState.Loading,
nostrRepository: NostrRepository,
chatRepository: ChatRepository
@@ -80,6 +85,7 @@ class ChatRoomDetailViewModel(
initializer {
ChatRoomDetailViewModel(
directMessageId = publicKey,
relayHint = relayHint,
initialChatRoomDetailUIState = initialChatRoomDetailUIState,
nostrRepository = nostrRepository,
chatRepository = chatRepository

View File

@@ -144,7 +144,8 @@ class ChatRoomListViewModel(
onClick = {
onNavigateToDirectMessageDetail.invoke(
ChatRoomDetailRoute(
directMessageIdentifier = localChatRoom.chatRoom.id
directMessageIdentifier = localChatRoom.chatRoom.id,
relayHint = localChatRoom.localParticipants.firstOrNull { it.participant.participantPublicKey != localChatRoom.chatRoom.userPublicKey }?.participant?.relayHint
)
)
}