diff --git a/composeApp/schemas/ac.cord.auxiliary.compose.database.AuxDatabase/1.json b/composeApp/schemas/ac.cord.auxiliary.compose.database.AuxDatabase/1.json index dfec12a5..9822f930 100644 --- a/composeApp/schemas/ac.cord.auxiliary.compose.database.AuxDatabase/1.json +++ b/composeApp/schemas/ac.cord.auxiliary.compose.database.AuxDatabase/1.json @@ -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')" ] } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/AuxDatabase.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/AuxDatabase.kt index bf63c28a..b035576f 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/AuxDatabase.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/AuxDatabase.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ChatRoomDao.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ChatRoomDao.kt index 5fb5a2b7..ca1618b6 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ChatRoomDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ChatRoomDao.kt @@ -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> diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt index 7b1fb686..87c6eac2 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrDao.kt @@ -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, diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrNip17Dao.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrNip17Dao.kt index 94fe3284..988a6004 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrNip17Dao.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/NostrNip17Dao.kt @@ -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" + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ParticipantDao.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ParticipantDao.kt new file mode 100644 index 00000000..808c6444 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ParticipantDao.kt @@ -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) + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ProfileDao.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ProfileDao.kt index ac2f2f7d..a4b96bc3 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ProfileDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/ProfileDao.kt @@ -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): List + @Query("SELECT * FROM NostrEvent WHERE kind = 0 AND pubKey = :publicKey") fun observeProfileWithFollowersByPublicKey(publicKey: String): Flow diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalChatRoom.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalChatRoom.kt index 88f4b804..ed970f5f 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalChatRoom.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/intermdiate/LocalChatRoom.kt @@ -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 ) diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt index ae264dc8..efe9584d 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt @@ -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 ) } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt index 824aa7f7..9fd72f4c 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt @@ -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 } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt index 044a74b9..bb38d154 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt @@ -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( diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/AuxNavHost.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/AuxNavHost.kt index b0349852..c961cba6 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/AuxNavHost.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/AuxNavHost.kt @@ -357,8 +357,11 @@ fun AuxNavHost( } } - composable { + composable { backStackEntry -> + val route = backStackEntry.toRoute() + ChatRoomDetailScreen( + publicKey = route.directMessageIdentifier, nostrRepository = databaseNostrRepository, chatRepository = databaseChatRepository ) diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/ChatRoomDetailRoute.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/ChatRoomDetailRoute.kt index 4dd9665d..cb461e8c 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/ChatRoomDetailRoute.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/routes/ChatRoomDetailRoute.kt @@ -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() { } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/MetadataEventDetail.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/MetadataEventDetail.kt index 7ca91efe..4082b80f 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/MetadataEventDetail.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/widgets/detail/MetadataEventDetail.kt @@ -227,7 +227,7 @@ fun MetadataEventDetail( onClick = { onNavigateToChatRoom.invoke( ChatRoomDetailRoute( - directMessageIdentifier = localNostrEvent.nostrEvent.id + directMessageIdentifier = localNostrEvent.nostrEvent.pubKey ) ) }