getOrCreateChat using publicKey

This commit is contained in:
Kgothatso Ngako
2026-06-01 00:42:48 +02:00
parent c8ba6a0bf0
commit 29ec692fc4
14 changed files with 204 additions and 14 deletions

View File

@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "10c21ff08b8acfda26aa17864d1e940e",
"identityHash": "3594831027f19047682084075fd88ace",
"entities": [
{
"tableName": "BroadcastNostrEventReceipt",
@@ -1362,6 +1362,88 @@
}
]
},
{
"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 )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "participantPublicKey",
"columnName": "participantPublicKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "chatRoomId",
"columnName": "chatRoomId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updatedAt",
"columnName": "updatedAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "savedAt",
"columnName": "savedAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "deletedAt",
"columnName": "deletedAt",
"affinity": "INTEGER"
},
{
"fieldPath": "broadcastedAt",
"columnName": "broadcastedAt",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"foreignKeys": [
{
"table": "Profile",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"participantPublicKey"
],
"referencedColumns": [
"publicKey"
]
},
{
"table": "ChatRoom",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"chatRoomId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "Post",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `repostId` TEXT, `quote` TEXT, `content` TEXT NOT NULL, `nostrEventId` TEXT NOT NULL, `replyToId` TEXT, `profilePublicKey` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, `broadcastedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`profilePublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`nostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`replyToId`) REFERENCES `Post`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`repostId`) REFERENCES `Post`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
@@ -2543,7 +2625,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, '10c21ff08b8acfda26aa17864d1e940e')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3594831027f19047682084075fd88ace')"
]
}
}

View File

@@ -17,6 +17,7 @@ import ac.cord.auxiliary.compose.database.dao.NostrDao
import ac.cord.auxiliary.compose.database.dao.NostrEventDao
import ac.cord.auxiliary.compose.database.dao.NostrEventRelayDao
import ac.cord.auxiliary.compose.database.dao.NostrNip17Dao
import ac.cord.auxiliary.compose.database.dao.ParticipantDao
import ac.cord.auxiliary.compose.database.dao.PostDao
import ac.cord.auxiliary.compose.database.dao.ProfileDao
import ac.cord.auxiliary.compose.database.dao.QuotedRelationDao
@@ -42,6 +43,7 @@ import ac.cord.auxiliary.compose.database.model.NegentropySynchronizeRequest
import ac.cord.auxiliary.compose.database.model.NegentropySynchronizeResult
import ac.cord.auxiliary.compose.database.model.NostrEvent
import ac.cord.auxiliary.compose.database.model.NostrEventRelay
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.database.model.QuotedRelation
@@ -79,6 +81,7 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
NegentropySynchronizeResult::class,
NostrEvent::class,
NostrEventRelay::class,
Participant::class,
Post::class,
Profile::class,
QuotedRelation::class,
@@ -126,6 +129,8 @@ abstract class AuxDatabase: RoomDatabase() {
abstract fun nostrNip17Dao(): NostrNip17Dao
abstract fun participantDao(): ParticipantDao
abstract fun postDao(): PostDao
abstract fun profileDao(): ProfileDao

View File

@@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface ChatRoomDao {
@Query("SELECT * FROM ChatRoom WHERE id = :id")
fun findChatRoomById(id: String): ChatRoom?
fun findChatRoomById(id: String): LocalChatRoom?
@Query("SELECT * FROM ChatRoom WHERE userPublicKey = :userPublicKey")
fun observeChatRoomByUserPublicKey(userPublicKey: String): Flow<List<LocalChatRoom>>

View File

@@ -361,11 +361,11 @@ abstract class NostrDao(
).value.toHex()
// Process chatMessage and chatRoom stuff...
// Find or create chatRoom
val chatRoom = database.chatRoomDao().findChatRoomById(
val localChatRoom = database.chatRoomDao().findChatRoomById(
chatRoomId
)
if (chatRoom == null) {
if (localChatRoom == null) {
database.chatRoomDao().upsert(
ChatRoom(
id = chatRoomId,

View File

@@ -1,18 +1,87 @@
package ac.cord.auxiliary.compose.database.dao
import ac.cord.auxiliary.compose.database.AuxDatabase
import ac.cord.auxiliary.compose.database.model.ChatRoom
import ac.cord.auxiliary.compose.database.model.Participant
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
import ac.cord.auxiliary.compose.managers.SeedManager
import androidx.room3.Dao
import androidx.room3.Transaction
import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import fr.acinq.bitcoin.PublicKey
import fr.acinq.bitcoin.crypto.musig2.Musig2
import kotlin.math.log
@Dao
abstract class NostrNip17Dao(
private val database: AuxDatabase
) {
val logger = Logger.withTag(TAG)
@Transaction
open suspend fun getOrCreateChatRoom(publicKey: String): LocalChatRoom? {
open suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String? = null): LocalChatRoom? {
logger.d("getOrCreateChatRoom: $publicKey")
val activePublicKey = SeedManager.activePublicKey().toHexKey()
val hexKeys = listOf(
activePublicKey,
publicKey
).sorted()
logger.d("hexKeys: $hexKeys")
val publicKeys = hexKeys.map {
PublicKey(
PublicKey.compress(it.hexToByteArray())
)
}
logger.d("PublicKeys: $publicKeys")
val chatRoomXOnlyPublicKey = Musig2.aggregateKeys(
publicKeys
)
logger.d("ChatRoomXOnly: $chatRoomXOnlyPublicKey")
val chatRoomId = chatRoomXOnlyPublicKey.publicKey.value.toHex()
logger.d("chatRoomId: $chatRoomId")
val localChatRoom = database.chatRoomDao().findChatRoomById(chatRoomId)
if (localChatRoom != null) {
return localChatRoom
} else {
val profiles = database.profileDao().getAllProfiles()
if (profiles.isNotEmpty()) {
// Create new chatRoom
val chatRoom = ChatRoom(
id = chatRoomId,
userPublicKey = activePublicKey,
subject = defaultSubject,
)
database.chatRoomDao().upsert(chatRoom)
database.participantDao().upsert(
profiles.map {
Participant(
participantPublicKey = it.publicKey,
chatRoomId = chatRoomId
)
}
)
return LocalChatRoom(
chatRoom = chatRoom
)
}
}
return null
}
companion object {
const val TAG = "NostrNip17Dao"
}
}

View File

@@ -0,0 +1,22 @@
package ac.cord.auxiliary.compose.database.dao
import ac.cord.auxiliary.compose.database.model.ChatRoom
import ac.cord.auxiliary.compose.database.model.Participant
import ac.cord.auxiliary.compose.database.model.Relay
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
import androidx.room3.Dao
import androidx.room3.Query
import androidx.room3.Upsert
import fr.acinq.bitcoin.PublicKey
import kotlinx.coroutines.flow.Flow
@Dao
interface ParticipantDao {
@Query("SELECT * FROM Participant WHERE participantPublicKey = :participantPublicKey")
fun findParticipantByPublicKey(participantPublicKey: String): Participant?
@Upsert
suspend fun upsert(participants: List<Participant>)
}

View File

@@ -24,6 +24,9 @@ interface ProfileDao {
@Query("SELECT * FROM Profile WHERE publicKey = :publicKey")
fun getProfileByPublicKey(publicKey: String): Profile?
@Query("SELECT * FROM Profile WHERE publicKey IN (:publicKeys)")
fun getProfileByPublicKeys(publicKeys: List<String>): List<Profile>
@Query("SELECT * FROM NostrEvent WHERE kind = 0 AND pubKey = :publicKey")
fun observeProfileWithFollowersByPublicKey(publicKey: String): Flow<LocalProfileWithFollowers?>

View File

@@ -1,6 +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.Participant
import ac.cord.auxiliary.compose.database.model.Post
import ac.cord.auxiliary.compose.database.model.Profile
import androidx.room3.Embedded
@@ -8,4 +9,6 @@ import androidx.room3.Relation
data class LocalChatRoom(
@Embedded val chatRoom: ChatRoom,
// val participants: List<Participant>
)

View File

@@ -11,9 +11,10 @@ class DatabaseChatRepository(
private val scope: CoroutineScope
): ChatRepository {
override suspend fun getOrCreateChatRoom(publicKey: String): LocalChatRoom? {
override suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String?): LocalChatRoom? {
return database.nostrNip17Dao().getOrCreateChatRoom(
publicKey
publicKey,
defaultSubject = defaultSubject
)
}

View File

@@ -4,13 +4,13 @@ import ac.cord.auxiliary.compose.database.model.ChatRoom
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
interface ChatRepository {
suspend fun getOrCreateChatRoom(publicKey: String): LocalChatRoom?
suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String? = null): LocalChatRoom?
fun updateChatRoomSubject(publicKey: String, subject: String): ChatRoom?
companion object {
val NO_OP_CHAT_REPOSITORY = object: ChatRepository {
override suspend fun getOrCreateChatRoom(publicKey: String): LocalChatRoom? {
override suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String?): LocalChatRoom? {
return null
}

View File

@@ -36,13 +36,14 @@ import kotlin.time.Clock
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun ChatRoomDetailScreen(
publicKey: String,
initialChatRoomDetailUIState: ChatRoomDetailUIState = ChatRoomDetailUIState.Loading,
nostrRepository: NostrRepository,
chatRepository: ChatRepository
) {
val chatRoomDetailViewModel: ChatRoomDetailViewModel = viewModel(
factory = ChatRoomDetailViewModel.factory(
publicKey = "",
publicKey = publicKey,
initialChatRoomDetailUIState = initialChatRoomDetailUIState,
nostrRepository = nostrRepository,
chatRepository = chatRepository
@@ -136,6 +137,7 @@ private fun UnqueuedProfileSynchronizationScreenPreview() {
modifier = Modifier.fillMaxSize()
) {
ChatRoomDetailScreen(
publicKey = "publicKey",
initialChatRoomDetailUIState = ChatRoomDetailUIState.Loaded(
localChatRoom = LocalChatRoom(
chatRoom = ChatRoom(

View File

@@ -357,8 +357,11 @@ fun AuxNavHost(
}
}
composable<ChatRoomDetailRoute> {
composable<ChatRoomDetailRoute> { backStackEntry ->
val route = backStackEntry.toRoute<ChatRoomDetailRoute>()
ChatRoomDetailScreen(
publicKey = route.directMessageIdentifier,
nostrRepository = databaseNostrRepository,
chatRepository = databaseChatRepository
)

View File

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

View File

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