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 b1ab12c4..74fef65a 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": "65a72c3df37d4135fefcfb91e43296ff", + "identityHash": "24fc70446262631361657ac068483160", "entities": [ { "tableName": "BroadcastNostrEventReceipt", @@ -276,7 +276,7 @@ }, { "tableName": "ChatMessage", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `senderPublicKey` TEXT NOT NULL, `isUserMessage` INTEGER NOT NULL, `chatRoomId` TEXT NOT NULL, `giftWrapPayloadId` TEXT, `replyToMessageId` INTEGER, `quotedMessageId` INTEGER, `content` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, FOREIGN KEY(`chatRoomId`) REFERENCES `ChatRoom`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`giftWrapPayloadId`) REFERENCES `GiftWrapPayload`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `senderPublicKey` TEXT NOT NULL, `isUserMessage` INTEGER NOT NULL, `giftWrapPayloadId` INTEGER NOT NULL, `chatRoomId` TEXT NOT NULL, `replyToMessageId` INTEGER, `quotedMessageId` INTEGER, `content` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, FOREIGN KEY(`chatRoomId`) REFERENCES `ChatRoom`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`giftWrapPayloadId`) REFERENCES `GiftWrapPayload`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -296,17 +296,18 @@ "affinity": "INTEGER", "notNull": true }, + { + "fieldPath": "giftWrapPayloadId", + "columnName": "giftWrapPayloadId", + "affinity": "INTEGER", + "notNull": true + }, { "fieldPath": "chatRoomId", "columnName": "chatRoomId", "affinity": "TEXT", "notNull": true }, - { - "fieldPath": "giftWrapPayloadId", - "columnName": "giftWrapPayloadId", - "affinity": "TEXT" - }, { "fieldPath": "replyToMessageId", "columnName": "replyToMessageId", @@ -385,7 +386,7 @@ }, { "tableName": "ChatRoom", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `userPublicKey` TEXT NOT NULL, `subject` TEXT, `initialGiftWrapUnsignedId` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`userPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`initialGiftWrapUnsignedId`) REFERENCES `GiftWrapPayload`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `userPublicKey` TEXT NOT NULL, `subject` TEXT, `initialGiftWrapPayloadId` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`userPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`initialGiftWrapPayloadId`) REFERENCES `GiftWrapPayload`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -405,9 +406,9 @@ "affinity": "TEXT" }, { - "fieldPath": "initialGiftWrapUnsignedId", - "columnName": "initialGiftWrapUnsignedId", - "affinity": "TEXT" + "fieldPath": "initialGiftWrapPayloadId", + "columnName": "initialGiftWrapPayloadId", + "affinity": "INTEGER" }, { "fieldPath": "createdAt", @@ -461,7 +462,7 @@ "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ - "initialGiftWrapUnsignedId" + "initialGiftWrapPayloadId" ], "referencedColumns": [ "id" @@ -790,12 +791,12 @@ }, { "tableName": "GiftWrapPayload", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `kind` INTEGER NOT NULL, `tags` TEXT NOT NULL, `content` TEXT NOT NULL, `quotedEventId` TEXT, `giftWrapSealId` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`giftWrapSealId`) REFERENCES `GiftWrapSeal`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `publicKey` TEXT NOT NULL, `kind` INTEGER NOT NULL, `tags` TEXT NOT NULL, `content` TEXT NOT NULL, `quotedEventId` TEXT, `giftWrapSealId` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, FOREIGN KEY(`giftWrapSealId`) REFERENCES `GiftWrapSeal`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", "columnName": "id", - "affinity": "TEXT", + "affinity": "INTEGER", "notNull": true }, { @@ -862,7 +863,7 @@ } ], "primaryKey": { - "autoGenerate": false, + "autoGenerate": true, "columnNames": [ "id" ] @@ -2630,7 +2631,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, '65a72c3df37d4135fefcfb91e43296ff')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '24fc70446262631361657ac068483160')" ] } } \ 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 b035576f..ee611e50 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 @@ -55,7 +55,6 @@ import ac.cord.auxiliary.compose.database.model.SynchronizeNostrEventRequest import ac.cord.auxiliary.compose.database.model.SynchronizeNostrEventResult import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent import ac.cord.auxiliary.compose.database.model.Zap -import androidx.room3.AutoMigration import androidx.room3.Database import androidx.room3.RoomDatabase import androidx.room3.TypeConverters @@ -111,7 +110,7 @@ abstract class AuxDatabase: RoomDatabase() { abstract fun giftWrapSealDao(): GiftWrapSealDao - abstract fun giftWrapUnsignedDao(): GiftWrapPayloadDao + abstract fun giftWrapPayloadDao(): GiftWrapPayloadDao abstract fun inReplyToRelationDao(): InReplyToRelationDao diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/GiftWrapPayloadDao.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/GiftWrapPayloadDao.kt index 649e2ef6..fa041bdf 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/GiftWrapPayloadDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/dao/GiftWrapPayloadDao.kt @@ -4,13 +4,16 @@ import ac.cord.auxiliary.compose.database.model.GiftWrapPayload import androidx.room3.Dao import androidx.room3.Query import androidx.room3.Upsert +import kotlinx.coroutines.flow.Flow @Dao interface GiftWrapPayloadDao { @Query("SELECT * FROM GiftWrapPayload") fun getAllGiftWrapUnsigned(): List - @Upsert - suspend fun upsert(giftWrapPayload: GiftWrapPayload) + @Query("SELECT * FROM GiftWrapPayload WHERE publicKey = :publicKey AND giftWrapSealId IS NULL") + fun observeUnsealedGiftWrapPayloads(publicKey: String): Flow + @Upsert + suspend fun upsert(giftWrapPayload: GiftWrapPayload): Long } \ No newline at end of file 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 8e5cd1c3..e53e1959 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 @@ -347,11 +347,19 @@ abstract class NostrDao( giftWrapSeal.decryptGiftWrapPayload( activeUserPrivateKey - )?.let { giftWrapPayload -> - database.giftWrapUnsignedDao().upsert( - giftWrapPayload + )?.let { decryptedGiftWrapPayload -> + val result = database.giftWrapPayloadDao().upsert( + decryptedGiftWrapPayload ) + val giftWrapPayload = if (result != -1L) { + decryptedGiftWrapPayload.copy( + id = result + ) + } else { + decryptedGiftWrapPayload + } + val chatRoomId = giftWrapPayload.aggregatedParticipantsPublicKey() logger.d("ChatRoomId: $chatRoomId") @@ -368,7 +376,7 @@ abstract class NostrDao( userPublicKey = SeedManager.activeKeyPair().pubKey.toHex(), subject = null, // TODO: Get subject from tags... createdAt = giftWrapPayload.createdAt, - initialGiftWrapUnsignedId = giftWrapPayload.id + initialGiftWrapPayloadId = giftWrapPayload.id ) ) 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 b2d804af..76794a76 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,7 +1,11 @@ package ac.cord.auxiliary.compose.database.dao import ac.cord.auxiliary.compose.database.AuxDatabase +import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventRequest import ac.cord.auxiliary.compose.database.model.ChatRoom +import ac.cord.auxiliary.compose.database.model.GiftWrapMessage +import ac.cord.auxiliary.compose.database.model.GiftWrapSeal +import ac.cord.auxiliary.compose.database.model.NostrEvent import ac.cord.auxiliary.compose.database.model.Participant import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom import ac.cord.auxiliary.compose.managers.SeedManager @@ -9,6 +13,11 @@ import androidx.room3.Dao import androidx.room3.Transaction import co.touchlab.kermit.Logger import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import kotlin.time.Instant @Dao @@ -64,6 +73,66 @@ abstract class NostrNip17Dao( return null } + @Transaction + open suspend fun persistAndBroadcastGiftWrap( + sealedRumorEvent: SealedRumorEvent, + giftWrapEvent: GiftWrapEvent, + receiverPublicKey: PTag + ) { + + val nostrEvent = NostrEvent( + id = giftWrapEvent.id, + content = giftWrapEvent.content, + createdAt = Instant.fromEpochSeconds(giftWrapEvent.createdAt), + sig = giftWrapEvent.sig, + pubKey = giftWrapEvent.pubKey, + tags = giftWrapEvent.tags, + kind = giftWrapEvent.kind + ) + + + database.nostrEventDao().insert(nostrEvent) + + // TODO: Get ChatMessageRelayListEvent.KIND for receiverPublicKey.pubkey +// receiverPublicKey.relayHint?.url?.let { url -> +// database.broadcastNostrEventRequestDao().insert( +// listOf( +// BroadcastNostrEventRequest( +// nostrEventId = nostrEvent.id, +// relayURL = url +// ) +// ) +// ) +// } + + + database.giftWrapMessageDao().upsert( + GiftWrapMessage( + id = giftWrapEvent.id, + nostrEventId = nostrEvent.id, + content = giftWrapEvent.content, + createdAt = Instant.fromEpochSeconds(giftWrapEvent.createdAt), + signature = giftWrapEvent.sig, + publicKey = giftWrapEvent.pubKey, + kind = giftWrapEvent.kind, + receiverPublicKey = receiverPublicKey.pubKey, + receiverRelayHit = receiverPublicKey.relayHint?.url + ) + ) + + val giftWrapSeal = GiftWrapSeal( + id = sealedRumorEvent.id, + publicKey = sealedRumorEvent.pubKey, + content = sealedRumorEvent.content, + kind = sealedRumorEvent.kind, + tags = sealedRumorEvent.tags, + signature = sealedRumorEvent.sig, + giftWrapMessageId = giftWrapEvent.id + ) + + database.giftWrapSealDao().upsert(giftWrapSeal) + } + companion object { const val TAG = "NostrNip17Dao" } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/ChatMessage.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/ChatMessage.kt index 7020deb9..695b0b2d 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/ChatMessage.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/ChatMessage.kt @@ -34,8 +34,8 @@ data class ChatMessage( val senderPublicKey: String, val isUserMessage: Boolean, + val giftWrapPayloadId: Long, val chatRoomId: String, - val giftWrapPayloadId: String? = null, val replyToMessageId: Long? = null, val quotedMessageId: Long? = null, diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/ChatRoom.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/ChatRoom.kt index 1631e8e2..73b8e6f7 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/ChatRoom.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/ChatRoom.kt @@ -28,7 +28,7 @@ import kotlin.time.Instant ForeignKey( entity = GiftWrapPayload::class, parentColumns = ["id"], - childColumns = ["initialGiftWrapUnsignedId"], + childColumns = ["initialGiftWrapPayloadId"], onDelete = ForeignKey.CASCADE, ), ], @@ -69,7 +69,7 @@ data class ChatRoom( // TODO: Add a LastChatMessageRelation // val lastChatMessageId: String? = null, // Might want to focus on the other last messages... - val initialGiftWrapUnsignedId: String? = null, + val initialGiftWrapPayloadId: Long? = null, override val createdAt: Instant = Clock.System.now(), override val updatedAt: Instant = createdAt, @@ -77,7 +77,7 @@ data class ChatRoom( override val viewedAt: Instant? = null, override val deletedAt: Instant? = null, -): TimestampedEntity, LocalStoreEntity, UserViewableEntity, SoftDeletableEntity { + ): TimestampedEntity, LocalStoreEntity, UserViewableEntity, SoftDeletableEntity { companion object { const val TAG = "ChatRoom" diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/GiftWrapPayload.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/GiftWrapPayload.kt index 92d0ad5e..e3341154 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/GiftWrapPayload.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/GiftWrapPayload.kt @@ -30,8 +30,8 @@ import kotlin.time.Instant ], ) data class GiftWrapPayload( // TODO: Rename this to GiftWrapPayload... - @PrimaryKey - val id: HexKey, + @PrimaryKey(autoGenerate = true) + val id: Long = 0, /** * Sender Public Key diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/GiftWrapSeal.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/GiftWrapSeal.kt index d6175cf9..6aab42d3 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/GiftWrapSeal.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/model/GiftWrapSeal.kt @@ -96,7 +96,7 @@ data class GiftWrapSeal( logger.d("giftWrapPayload: ${giftWrapPayload.toJson()}") return GiftWrapPayload( - id = giftWrapPayload.id, + giftWrapSealId = id, publicKey = giftWrapPayload.pubKey, kind = giftWrapPayload.kind, tags = giftWrapPayload.tags, 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 6844364f..4b426d1e 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 @@ -8,9 +8,15 @@ import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatMessage import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom import ac.cord.auxiliary.compose.repository.ChatRepository import co.touchlab.kermit.Logger -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +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.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent -import fr.acinq.lightning.utils.UUID +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -49,15 +55,32 @@ class DatabaseChatRepository( override suspend fun sendChatMessage( text: String, - chatRoom: ChatRoom + localChatRoom: LocalChatRoom, + messageType: Kind ) { - logger.i("sendChatMessage($text): $chatRoom") + 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 giftWrapPayloadId = database.giftWrapPayloadDao().upsert( + GiftWrapPayload( + kind = messageType, + tags = receiverTags.toTypedArray(), // TODO: Get participants... + content = text, + publicKey = localChatRoom.chatRoom.userPublicKey + ) + ) database.chatMessageDao().upsert( ChatMessage( content = text, - chatRoomId = chatRoom.id, - senderPublicKey = chatRoom.userPublicKey, - isUserMessage = true + chatRoomId = localChatRoom.chatRoom.id, + senderPublicKey = localChatRoom.chatRoom.userPublicKey, + isUserMessage = true, + giftWrapPayloadId = giftWrapPayloadId ) ) } @@ -66,9 +89,67 @@ class DatabaseChatRepository( publicKey: String, subject: String ): ChatRoom? { + // TODO: Update chatRoom with subject... return null } + override suspend fun observeUnsealedGiftWrapPayloads(publicKey: HexKey): Flow { + return database.giftWrapPayloadDao().observeUnsealedGiftWrapPayloads(publicKey) + } + + override suspend fun sealGiftWrapPayload( + giftWrapPayload: GiftWrapPayload, + nostrSignerSync: NostrSignerSync + ) { + val giftWrapPayloadEvent = nostrSignerSync.signNormal( + createdAt = giftWrapPayload.createdAt.epochSeconds, + kind = giftWrapPayload.kind, + tags = giftWrapPayload.tags, + content = giftWrapPayload.content + ) + + giftWrapPayload.tags.taggedUsers().forEachIndexed { index, receiverPublicKey -> + val payload = giftWrapPayloadEvent.toJson() + + val sealedRumorEvent = nostrSignerSync.signNormal( + createdAt = TimeUtils.randomWithTwoDays(), + kind = SealedRumorEvent.KIND, + tags = emptyArray(), + content = nostrSignerSync.nip44Encrypt( + plaintext = payload, + toPublicKey = receiverPublicKey.pubKey + ) + ) + + val giftWrapEvent = nostrSignerSync.signNormal( + createdAt = TimeUtils.randomWithTwoDays(), + kind = GiftWrapEvent.KIND, + tags = arrayOf( + PTag.assemble(receiverPublicKey.pubKey, receiverPublicKey.relayHint) + ), + content = nostrSignerSync.nip44Encrypt( + plaintext = sealedRumorEvent.toJson(), + toPublicKey = receiverPublicKey.pubKey + ) + ) + + database.nostrNip17Dao().persistAndBroadcastGiftWrap( + sealedRumorEvent = sealedRumorEvent, + giftWrapEvent = giftWrapEvent, + receiverPublicKey = receiverPublicKey + ) + + if (index == 0 && giftWrapPayload.giftWrapSealId == null) { + // Only update the giftWrapPayload.giftWrapSealId on the first seal... + database.giftWrapPayloadDao().upsert( + giftWrapPayload.copy( + giftWrapSealId = sealedRumorEvent.id + ) + ) + } + } + } + companion object { const val TAG = "DatabaseChatRepository" } 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 59b32ca4..6a7aadc1 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 @@ -1,8 +1,14 @@ 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 import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom +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 kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -17,10 +23,18 @@ interface ChatRepository { suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String? = null): LocalChatRoom? - suspend fun sendChatMessage(text: String, chatRoom: ChatRoom) + suspend fun sendChatMessage( + text: String, + localChatRoom: LocalChatRoom, + messageType: Kind = ChatMessageEvent.KIND + ) fun updateChatRoomSubject(publicKey: String, subject: String): ChatRoom? + suspend fun observeUnsealedGiftWrapPayloads(publicKey: HexKey): Flow + + suspend fun sealGiftWrapPayload(giftWrapPayload: GiftWrapPayload, nostrSignerSync: NostrSignerSync) + companion object { val NO_OP_CHAT_REPOSITORY = object: ChatRepository { override suspend fun getChatRoomListByPublicKey(publicKey: String): List { @@ -46,7 +60,8 @@ interface ChatRepository { override suspend fun sendChatMessage( text: String, - chatRoom: ChatRoom + localChatRoom: LocalChatRoom, + messageType: Kind ) { // TODO("Not yet implemented") } @@ -54,6 +69,17 @@ interface ChatRepository { override fun updateChatRoomSubject(publicKey: String, subject: String): ChatRoom? { return null } + + override suspend fun observeUnsealedGiftWrapPayloads(publicKey: HexKey): Flow { + TODO("Not yet implemented") + } + + override suspend fun sealGiftWrapPayload( + giftWrapPayload: GiftWrapPayload, + nostrSignerSync: NostrSignerSync + ) { + TODO("Not yet implemented") + } } } } \ No newline at end of file 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 dab3e6d7..d71bdb95 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 @@ -1,49 +1,31 @@ package ac.cord.auxiliary.compose.ui.composable 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.repository.ChatRepository import ac.cord.auxiliary.compose.repository.NostrRepository import ac.cord.auxiliary.compose.ui.composable.widgets.LoadingDataIndicator -import ac.cord.auxiliary.compose.ui.composable.widgets.profile.ProfileColor import ac.cord.auxiliary.compose.ui.theme.AuxTheme import ac.cord.auxiliary.compose.ui.view.model.ChatRoomDetailMessageListViewModel import ac.cord.auxiliary.compose.ui.view.model.ChatRoomDetailViewModel -import ac.cord.auxiliary.compose.ui.view.state.ChatRoomDetailMessageListUIState import ac.cord.auxiliary.compose.ui.view.state.ChatRoomDetailUIState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AlternateEmail -import androidx.compose.material.icons.filled.BrowseGallery -import androidx.compose.material.icons.filled.Collections -import androidx.compose.material.icons.filled.Email -import androidx.compose.material.icons.filled.EmojiFlags -import androidx.compose.material.icons.filled.InsertEmoticon -import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.Mic -import androidx.compose.material.icons.filled.Pin -import androidx.compose.material.icons.filled.PinDrop -import androidx.compose.material.icons.filled.RecordVoiceOver import androidx.compose.material.icons.filled.Send -import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBarDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FlexibleBottomAppBar import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -52,25 +34,17 @@ import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextField import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.key -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import kotlin.time.Clock @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @Composable @@ -300,7 +274,7 @@ private fun ChatRoomDetailScreenPreview() { id = "", userPublicKey = "", subject = "Message title", - initialGiftWrapUnsignedId = "" + initialGiftWrapPayloadId = 0 ), ) ), 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 1fa0c313..33385db8 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 @@ -105,6 +105,7 @@ fun AuxNavHost( factory = NavigationViewModel.factory( initialNavigationUIState = NavigationUIState.Loading, nostrRepository = databaseNostrRepository, + chatRepository = databaseChatRepository, relayRepository = databaseNostrRepository, scope = applicationIOScope ) diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomDetailMessageListViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomDetailMessageListViewModel.kt index 9d4854a8..d0fc1044 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomDetailMessageListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomDetailMessageListViewModel.kt @@ -260,7 +260,7 @@ class ChatRoomDetailMessageListViewModel( viewModelScope.launch(Dispatchers.IO) { chatRepository.sendChatMessage( text = text, - chatRoom = localChatRoom.chatRoom + localChatRoom = localChatRoom ) } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NavigationViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NavigationViewModel.kt index 3e27f4de..b7818a8b 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NavigationViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/NavigationViewModel.kt @@ -1,11 +1,11 @@ package ac.cord.auxiliary.compose.ui.view.model import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventRequest +import ac.cord.auxiliary.compose.database.model.GiftWrapSeal import ac.cord.auxiliary.compose.database.model.NostrEvent import ac.cord.auxiliary.compose.database.model.SynchronizeNostrEventRequest import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter import ac.cord.auxiliary.compose.managers.SeedManager -import ac.cord.auxiliary.compose.network.dto.RelayDTO import ac.cord.auxiliary.compose.network.dto.toRelayDTO import ac.cord.auxiliary.compose.network.relays.RelayPool.Companion.PUBLISH_TIMEOUT import ac.cord.auxiliary.compose.network.relays.RelaysSocketManager @@ -13,6 +13,7 @@ import ac.cord.auxiliary.compose.network.sockets.NostrIncomingMessage import ac.cord.auxiliary.compose.network.sockets.NostrSocketClientFactory import ac.cord.auxiliary.compose.nostr.Relays 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 ac.cord.auxiliary.compose.ui.view.state.NavigationUIState @@ -29,9 +30,15 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CloseCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers +import com.vitorpamplona.quartz.nip17Dm.NIP17Factory +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip77Negentropy.NegCloseCmd import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -52,6 +59,7 @@ import kotlin.time.Instant class NavigationViewModel( initialNavigationUIState: NavigationUIState, val nostrRepository: NostrRepository, + val chatRepository: ChatRepository, val relayRepository: RelayRepository, val scope: CoroutineScope, ): ViewModel() { @@ -70,6 +78,7 @@ class NavigationViewModel( fun factory( initialNavigationUIState: NavigationUIState, nostrRepository: NostrRepository, + chatRepository: ChatRepository, relayRepository: RelayRepository, scope: CoroutineScope, ): ViewModelProvider.Factory = viewModelFactory { @@ -77,6 +86,7 @@ class NavigationViewModel( NavigationViewModel( initialNavigationUIState = initialNavigationUIState, nostrRepository = nostrRepository, + chatRepository = chatRepository, relayRepository = relayRepository, scope = scope ) @@ -93,7 +103,7 @@ class NavigationViewModel( init { observeUnsignedNostrEvents() - // TODO: observePendingUnsignedChatMessages + observeUnsealedGiftWrapPayloads() observePendingBroadcastNostrEventRequests() observeProfile() observePendingSyncNostrEventRequests() @@ -141,6 +151,29 @@ class NavigationViewModel( } } + private fun observeUnsealedGiftWrapPayloads() { + val tempSigner = NostrSignerSync( + SeedManager.activeKeyPair() + ) + scope.launch(Dispatchers.IO) { + logger.i { "observeUnsealedGiftWrapPayloads" } + chatRepository.observeUnsealedGiftWrapPayloads( + publicKey = SeedManager.activePublicKey().toHexKey() + ).distinctUntilChanged().collect { giftWrapPayloadOrNull -> + scope.launch(Dispatchers.IO) { + giftWrapPayloadOrNull?.let { giftWrapPayload -> + logger.d("seal and deliver giftWrapPayload: $giftWrapPayload") + + chatRepository.sealGiftWrapPayload( + giftWrapPayload, + nostrSignerSync = tempSigner + ) + } + } + } + } + } + private fun observePendingSyncNostrEventRequests() { logger.i { "observePendingSyncNostrEventRequests" }