Add chatroom functionality

This commit is contained in:
Kgothatso Ngako
2026-05-18 22:52:09 +02:00
parent 51220dbf5e
commit 7041fefbbd
19 changed files with 922 additions and 2799 deletions

View File

@@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "49c580a762a1c128bf5b4fc6f93eeea4", "identityHash": "6eec904dd8f91ab435301e23425eb536",
"entities": [ "entities": [
{ {
"tableName": "BroadcastNostrEventReceipt", "tableName": "BroadcastNostrEventReceipt",
@@ -274,6 +274,197 @@
} }
] ]
}, },
{
"tableName": "ChatMessage",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `senderPublicKey` TEXT NOT NULL, `isUserMessage` INTEGER NOT NULL, `chatRoomId` TEXT NOT NULL, `replyToEventId` TEXT, `quotedEventId` TEXT, `content` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`chatRoomId`) REFERENCES `ChatRoom`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`id`) REFERENCES `GiftWrapPayload`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "senderPublicKey",
"columnName": "senderPublicKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isUserMessage",
"columnName": "isUserMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "chatRoomId",
"columnName": "chatRoomId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "replyToEventId",
"columnName": "replyToEventId",
"affinity": "TEXT"
},
{
"fieldPath": "quotedEventId",
"columnName": "quotedEventId",
"affinity": "TEXT"
},
{
"fieldPath": "content",
"columnName": "content",
"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": "viewedAt",
"columnName": "viewedAt",
"affinity": "INTEGER"
},
{
"fieldPath": "deletedAt",
"columnName": "deletedAt",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"foreignKeys": [
{
"table": "ChatRoom",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"chatRoomId"
],
"referencedColumns": [
"id"
]
},
{
"table": "GiftWrapPayload",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "ChatRoom",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `userPublicKey` TEXT NOT NULL, `subject` TEXT, `initialGiftWrapUnsignedId` TEXT NOT NULL, `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 )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userPublicKey",
"columnName": "userPublicKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "subject",
"columnName": "subject",
"affinity": "TEXT"
},
{
"fieldPath": "initialGiftWrapUnsignedId",
"columnName": "initialGiftWrapUnsignedId",
"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": "viewedAt",
"columnName": "viewedAt",
"affinity": "INTEGER"
},
{
"fieldPath": "deletedAt",
"columnName": "deletedAt",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"foreignKeys": [
{
"table": "Profile",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"userPublicKey"
],
"referencedColumns": [
"publicKey"
]
},
{
"table": "GiftWrapPayload",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"initialGiftWrapUnsignedId"
],
"referencedColumns": [
"id"
]
}
]
},
{ {
"tableName": "InReplyToRelation", "tableName": "InReplyToRelation",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`replyingNostrEventId` TEXT NOT NULL, `inReplyToNostrEventId` TEXT NOT NULL, `inReplyToProfilePublicKey` TEXT, `inReplyToRootNostrEventId` TEXT, `inReplyToRootProfilePublicKey` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, PRIMARY KEY(`replyingNostrEventId`, `inReplyToNostrEventId`), FOREIGN KEY(`inReplyToProfilePublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`inReplyToRootProfilePublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`replyingNostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`inReplyToNostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`inReplyToRootNostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`replyingNostrEventId` TEXT NOT NULL, `inReplyToNostrEventId` TEXT NOT NULL, `inReplyToProfilePublicKey` TEXT, `inReplyToRootNostrEventId` TEXT, `inReplyToRootProfilePublicKey` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, PRIMARY KEY(`replyingNostrEventId`, `inReplyToNostrEventId`), FOREIGN KEY(`inReplyToProfilePublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`inReplyToRootProfilePublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`replyingNostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`inReplyToNostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`inReplyToRootNostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
@@ -399,6 +590,293 @@
} }
] ]
}, },
{
"tableName": "GiftWrapMessage",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `kind` INTEGER NOT NULL, `receiverPublicKey` TEXT NOT NULL, `receiverRelayHit` TEXT, `content` TEXT NOT NULL, `signature` TEXT NOT NULL, `nostrEventId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`nostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "publicKey",
"columnName": "publicKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "kind",
"columnName": "kind",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "receiverPublicKey",
"columnName": "receiverPublicKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "receiverRelayHit",
"columnName": "receiverRelayHit",
"affinity": "TEXT"
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "signature",
"columnName": "signature",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "nostrEventId",
"columnName": "nostrEventId",
"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": "viewedAt",
"columnName": "viewedAt",
"affinity": "INTEGER"
},
{
"fieldPath": "deletedAt",
"columnName": "deletedAt",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"foreignKeys": [
{
"table": "NostrEvent",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"nostrEventId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "GiftWrapSeal",
"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, `signature` TEXT NOT NULL, `giftWrapMessageId` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`giftWrapMessageId`) REFERENCES `GiftWrapMessage`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "publicKey",
"columnName": "publicKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "kind",
"columnName": "kind",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "signature",
"columnName": "signature",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "giftWrapMessageId",
"columnName": "giftWrapMessageId",
"affinity": "TEXT"
},
{
"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": "viewedAt",
"columnName": "viewedAt",
"affinity": "INTEGER"
},
{
"fieldPath": "deletedAt",
"columnName": "deletedAt",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"foreignKeys": [
{
"table": "GiftWrapMessage",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"giftWrapMessageId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"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 )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "publicKey",
"columnName": "publicKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "kind",
"columnName": "kind",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "quotedEventId",
"columnName": "quotedEventId",
"affinity": "TEXT"
},
{
"fieldPath": "giftWrapSealId",
"columnName": "giftWrapSealId",
"affinity": "TEXT"
},
{
"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": "viewedAt",
"columnName": "viewedAt",
"affinity": "INTEGER"
},
{
"fieldPath": "deletedAt",
"columnName": "deletedAt",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"foreignKeys": [
{
"table": "GiftWrapSeal",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"giftWrapSealId"
],
"referencedColumns": [
"id"
]
}
]
},
{ {
"tableName": "Mention", "tableName": "Mention",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mentionedPublicKey` TEXT NOT NULL, `mentioningNostrEventId` TEXT, `relayUrl` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, FOREIGN KEY(`mentionedPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`mentioningNostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mentionedPublicKey` TEXT NOT NULL, `mentioningNostrEventId` TEXT, `relayUrl` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `deletedAt` INTEGER, `broadcastedAt` INTEGER, FOREIGN KEY(`mentionedPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`mentioningNostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
@@ -2066,7 +2544,7 @@
], ],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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, '49c580a762a1c128bf5b4fc6f93eeea4')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6eec904dd8f91ab435301e23425eb536')"
] ]
} }
} }

View File

@@ -8,7 +8,7 @@ import ac.cord.auxiliary.compose.database.dao.ChatRoomDao
import ac.cord.auxiliary.compose.database.dao.ConnectionDao import ac.cord.auxiliary.compose.database.dao.ConnectionDao
import ac.cord.auxiliary.compose.database.dao.GiftWrapMessageDao import ac.cord.auxiliary.compose.database.dao.GiftWrapMessageDao
import ac.cord.auxiliary.compose.database.dao.GiftWrapSealDao import ac.cord.auxiliary.compose.database.dao.GiftWrapSealDao
import ac.cord.auxiliary.compose.database.dao.GiftWrapUnsignedDao import ac.cord.auxiliary.compose.database.dao.GiftWrapPayloadDao
import ac.cord.auxiliary.compose.database.dao.InReplyToRelationDao import ac.cord.auxiliary.compose.database.dao.InReplyToRelationDao
import ac.cord.auxiliary.compose.database.dao.MentionDao import ac.cord.auxiliary.compose.database.dao.MentionDao
import ac.cord.auxiliary.compose.database.dao.NegentropySynchronizeRequestDao import ac.cord.auxiliary.compose.database.dao.NegentropySynchronizeRequestDao
@@ -34,7 +34,7 @@ import ac.cord.auxiliary.compose.database.model.ChatRoom
import ac.cord.auxiliary.compose.database.model.Connection import ac.cord.auxiliary.compose.database.model.Connection
import ac.cord.auxiliary.compose.database.model.GiftWrapMessage import ac.cord.auxiliary.compose.database.model.GiftWrapMessage
import ac.cord.auxiliary.compose.database.model.GiftWrapSeal import ac.cord.auxiliary.compose.database.model.GiftWrapSeal
import ac.cord.auxiliary.compose.database.model.GiftWrapUnsigned import ac.cord.auxiliary.compose.database.model.GiftWrapPayload
import ac.cord.auxiliary.compose.database.model.InReplyToRelation import ac.cord.auxiliary.compose.database.model.InReplyToRelation
import ac.cord.auxiliary.compose.database.model.Mention import ac.cord.auxiliary.compose.database.model.Mention
import ac.cord.auxiliary.compose.database.model.NegentropySynchronizeRequest import ac.cord.auxiliary.compose.database.model.NegentropySynchronizeRequest
@@ -72,7 +72,7 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
InReplyToRelation::class, InReplyToRelation::class,
GiftWrapMessage::class, GiftWrapMessage::class,
GiftWrapSeal::class, GiftWrapSeal::class,
GiftWrapUnsigned::class, GiftWrapPayload::class,
Mention::class, Mention::class,
NegentropySynchronizeRequest::class, NegentropySynchronizeRequest::class,
NegentropySynchronizeResult::class, NegentropySynchronizeResult::class,
@@ -90,13 +90,7 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
UnsignedNostrEvent::class, UnsignedNostrEvent::class,
Zap::class Zap::class
], ],
version = 2, version = 1
autoMigrations = [
AutoMigration(
from = 1,
to = 2
)
]
) )
@TypeConverters(AuxConverters::class) @TypeConverters(AuxConverters::class)
abstract class AuxDatabase: RoomDatabase() { abstract class AuxDatabase: RoomDatabase() {
@@ -113,7 +107,7 @@ abstract class AuxDatabase: RoomDatabase() {
abstract fun giftWrapSealDao(): GiftWrapSealDao abstract fun giftWrapSealDao(): GiftWrapSealDao
abstract fun giftWrapUnsignedDao(): GiftWrapUnsignedDao abstract fun giftWrapUnsignedDao(): GiftWrapPayloadDao
abstract fun inReplyToRelationDao(): InReplyToRelationDao abstract fun inReplyToRelationDao(): InReplyToRelationDao

View File

@@ -2,6 +2,7 @@ package ac.cord.auxiliary.compose.database.dao
import ac.cord.auxiliary.compose.database.model.ChatRoom import ac.cord.auxiliary.compose.database.model.ChatRoom
import ac.cord.auxiliary.compose.database.model.Relay import ac.cord.auxiliary.compose.database.model.Relay
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
import androidx.room3.Dao import androidx.room3.Dao
import androidx.room3.Query import androidx.room3.Query
import androidx.room3.Upsert import androidx.room3.Upsert
@@ -10,9 +11,11 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface ChatRoomDao { interface ChatRoomDao {
@Query("SELECT * FROM ChatRoom WHERE id = :id")
fun findChatRoomById(id: String): ChatRoom?
@Query("SELECT * FROM ChatRoom WHERE userPublicKey = :userPublicKey") @Query("SELECT * FROM ChatRoom WHERE userPublicKey = :userPublicKey")
fun observeChatRoomByUserPublicKey(userPublicKey: String): Flow<List<ChatRoom>> fun observeChatRoomByUserPublicKey(userPublicKey: String): Flow<List<LocalChatRoom>>
@Upsert @Upsert
suspend fun upsert(chatRoom: ChatRoom) suspend fun upsert(chatRoom: ChatRoom)

View File

@@ -0,0 +1,16 @@
package ac.cord.auxiliary.compose.database.dao
import ac.cord.auxiliary.compose.database.model.GiftWrapPayload
import androidx.room3.Dao
import androidx.room3.Query
import androidx.room3.Upsert
@Dao
interface GiftWrapPayloadDao {
@Query("SELECT * FROM GiftWrapPayload")
fun getAllGiftWrapUnsigned(): List<GiftWrapPayload>
@Upsert
suspend fun upsert(giftWrapPayload: GiftWrapPayload)
}

View File

@@ -1,19 +0,0 @@
package ac.cord.auxiliary.compose.database.dao
import ac.cord.auxiliary.compose.database.model.GiftWrapUnsigned
import ac.cord.auxiliary.compose.database.model.Relay
import androidx.room3.Dao
import androidx.room3.Query
import androidx.room3.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
interface GiftWrapUnsignedDao {
@Query("SELECT * FROM GiftWrapUnsigned")
fun getAllGiftWrapUnsigned(): List<GiftWrapUnsigned>
@Upsert
suspend fun upsert(giftWrapUnsigned: GiftWrapUnsigned)
}

View File

@@ -3,6 +3,8 @@ package ac.cord.auxiliary.compose.database.dao
import ac.cord.auxiliary.compose.database.AuxDatabase import ac.cord.auxiliary.compose.database.AuxDatabase
import ac.cord.auxiliary.compose.database.GENESIS_AT import ac.cord.auxiliary.compose.database.GENESIS_AT
import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventRequest import ac.cord.auxiliary.compose.database.model.BroadcastNostrEventRequest
import ac.cord.auxiliary.compose.database.model.ChatMessage
import ac.cord.auxiliary.compose.database.model.ChatRoom
import ac.cord.auxiliary.compose.database.model.Connection import ac.cord.auxiliary.compose.database.model.Connection
import ac.cord.auxiliary.compose.database.model.Mention import ac.cord.auxiliary.compose.database.model.Mention
import ac.cord.auxiliary.compose.database.model.NegentropySynchronizeRequest import ac.cord.auxiliary.compose.database.model.NegentropySynchronizeRequest
@@ -10,24 +12,21 @@ import ac.cord.auxiliary.compose.database.model.NostrEvent
import ac.cord.auxiliary.compose.database.model.NostrEventRelay import ac.cord.auxiliary.compose.database.model.NostrEventRelay
import ac.cord.auxiliary.compose.database.model.Post import ac.cord.auxiliary.compose.database.model.Post
import ac.cord.auxiliary.compose.database.model.Profile import ac.cord.auxiliary.compose.database.model.Profile
import ac.cord.auxiliary.compose.database.model.SynchronizeNostrEventRequest
import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent
import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
import ac.cord.auxiliary.compose.network.serialization.encodeToJsonString import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.nostr.Relays import ac.cord.auxiliary.compose.managers.toHex
import androidx.room3.Dao import androidx.room3.Dao
import androidx.room3.Transaction import androidx.room3.Transaction
import androidx.room3.useWriterConnection
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip18Reposts.quotes.QAddressableTag import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip18Reposts.quotes.QEventTag import fr.acinq.bitcoin.crypto.musig2.Musig2
import com.vitorpamplona.quartz.nip18Reposts.quotes.firstTaggedQuote
import kotlin.math.ceil
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Instant
@Dao @Dao
abstract class NostrDao( abstract class NostrDao(
@@ -124,7 +123,7 @@ abstract class NostrDao(
} else { } else {
// Nothing else needs to be done // Nothing else needs to be done
val profile = database.profileDao().getProfileByPublicKey(nostrEvent.pubKey) val profile = database.profileDao().getProfileByPublicKey(nostrEvent.pubKey)
if (profile?.createdAt == GENESIS_AT) { if (profile?.createdAt == GENESIS_AT && nostrEvent.kind == MetadataEvent.KIND) {
logger.i("This is a placeholder profile might need to get synced...: ${profile.publicKey}") logger.i("This is a placeholder profile might need to get synced...: ${profile.publicKey}")
// Setup the sync here... // Setup the sync here...
} else { } else {
@@ -158,13 +157,13 @@ abstract class NostrDao(
val profilePublicKeysToSync = mutableMapOf<String, MutableSet<String>>() val profilePublicKeysToSync = mutableMapOf<String, MutableSet<String>>()
val eventIdsToSync = mutableMapOf<String, MutableSet<String>>() val eventIdsToSync = mutableMapOf<String, MutableSet<String>>()
if (nostrEvent.unsignedNostrEventId == null) { if (nostrEvent.unsignedNostrEventId == null && nostrEvent.kind != GiftWrapEvent.KIND) {
// Find or create profile with the pubKey... if not found submit a sync request... // Find or create profile with the pubKey... if not found submit a sync request...
val profile = database.profileDao().getProfileByPublicKey(nostrEvent.pubKey) val profile = database.profileDao().getProfileByPublicKey(nostrEvent.pubKey)
if (profile == null) { if (profile == null) {
val placeHolderProfile = Profile( val placeHolderProfile = Profile(
displayName = "LOADING PROFILE...", displayName = "LOADING...",
publicKey = nostrEvent.pubKey, publicKey = nostrEvent.pubKey,
createdAt = GENESIS_AT, createdAt = GENESIS_AT,
nostrEventId = nostrEvent.id, // Will get overwriting by sync, nostrEventId = nostrEvent.id, // Will get overwriting by sync,
@@ -185,6 +184,67 @@ abstract class NostrDao(
logger.w("We somehow have an unsignedNostrEvent: $nostrEvent") logger.w("We somehow have an unsignedNostrEvent: $nostrEvent")
} }
// Index reposts first because they have the contained posts...
nostrEvent.toRepostedRelationWithContainedPost()?.let { (repostedRelation, containedPost) ->
// Find or create placeHolder note that is being reposted
val repostedNostrEvent = database.nostrEventDao().getNostrEventById(repostedRelation.repostedNostrEventId)
val repostedProfilePublicKey = if (repostedNostrEvent == null) {
// Sync event...
if (eventIdsToSync[relayURL] == null) {
eventIdsToSync[relayURL] = mutableSetOf()
}
eventIdsToSync[relayURL]?.add(repostedRelation.repostedNostrEventId)
database.nostrEventDao().insert(
if (containedPost != null) {
NostrEvent(
id = containedPost.id,
content = containedPost.content,
createdAt = Instant.fromEpochSeconds(containedPost.createdAt),
pubKey = containedPost.pubKey,
sig = containedPost.sig,
kind = containedPost.kind,
tags = containedPost.tags
)
} else {
NostrEvent(
id = repostedRelation.repostedNostrEventId,
content = "Pending Sync",
createdAt = GENESIS_AT,
pubKey = nostrEvent.pubKey,
sig = "",
kind = nostrEvent.kind,
tags = emptyArray()
)
}
)
repostedRelation.repostedProfilePublicKey
} else {
repostedRelation.repostedProfilePublicKey ?: repostedNostrEvent.pubKey
}
val repostedProfile = repostedProfilePublicKey?.let { database.profileDao().getProfileByPublicKey(it) }
if (repostedProfilePublicKey != null && repostedProfile == null) {
database.profileDao().insertPlaceholderProfile(
Profile(
displayName = "LOADING...",
publicKey = repostedProfilePublicKey,
createdAt = GENESIS_AT,
nostrEventId = nostrEvent.id, // Will get overwriting by sync,
)
)
}
database.repostedRelationDao().upsert(
repostedRelation.copy(
repostedProfilePublicKey = repostedProfilePublicKey
)
)
}
// Sync tagged events and authors... // Sync tagged events and authors...
nostrEvent.tags.taggedEvents().take(4).forEach { taggedEvent -> // We only look at the first 3 tagged events... nostrEvent.tags.taggedEvents().take(4).forEach { taggedEvent -> // We only look at the first 3 tagged events...
val taggedNostrEvent = database.nostrEventDao().getNostrEventById(taggedEvent.eventId) val taggedNostrEvent = database.nostrEventDao().getNostrEventById(taggedEvent.eventId)
@@ -227,7 +287,7 @@ abstract class NostrDao(
// Save a placeholder // Save a placeholder
database.profileDao().insertPlaceholderProfile( database.profileDao().insertPlaceholderProfile(
Profile( Profile(
displayName = "LOADING PROFILE...", displayName = "LOADING...",
publicKey = taggedUser.pubKey, publicKey = taggedUser.pubKey,
createdAt = GENESIS_AT, createdAt = GENESIS_AT,
nostrEventId = nostrEvent.id, // Will get overwriting by sync, nostrEventId = nostrEvent.id, // Will get overwriting by sync,
@@ -243,11 +303,10 @@ abstract class NostrDao(
) )
) )
if (recommendedRelay != null) { if (recommendedRelay != null) {
if (profilePublicKeysToSync[relayURL] == null) { if (profilePublicKeysToSync[recommendedRelay] == null) {
profilePublicKeysToSync[relayURL] = mutableSetOf() profilePublicKeysToSync[recommendedRelay] = mutableSetOf()
} }
profilePublicKeysToSync[relayURL]?.add(taggedUser.pubKey) profilePublicKeysToSync[recommendedRelay]?.add(taggedUser.pubKey)
} else { } else {
if (profilePublicKeysToSync[relayURL] == null) { if (profilePublicKeysToSync[relayURL] == null) {
profilePublicKeysToSync[relayURL] = mutableSetOf() profilePublicKeysToSync[relayURL] = mutableSetOf()
@@ -262,6 +321,100 @@ abstract class NostrDao(
database.profileDao().upsert(profile) database.profileDao().upsert(profile)
} }
nostrEvent.toGiftWrapMessageWithReceiverPTag()?.let { giftWrapMessage ->
val giftWrapReceiverProfile = database.profileDao().getProfileByPublicKey(giftWrapMessage.receiverPublicKey)
if (giftWrapReceiverProfile == null) {
// Save a placeholder
database.profileDao().insertPlaceholderProfile(
Profile(
displayName = "LOADING...",
publicKey = giftWrapMessage.receiverPublicKey,
createdAt = GENESIS_AT,
nostrEventId = nostrEvent.id, // Will get overwriting by sync,
)
)
val recommendRelayUrl = giftWrapMessage.receiverRelayHit
if (recommendRelayUrl != null) {
if (profilePublicKeysToSync[recommendRelayUrl] == null) {
profilePublicKeysToSync[recommendRelayUrl] = mutableSetOf()
}
profilePublicKeysToSync[recommendRelayUrl]?.add(giftWrapMessage.receiverPublicKey)
} else {
if (profilePublicKeysToSync[relayURL] == null) {
profilePublicKeysToSync[relayURL] = mutableSetOf()
}
profilePublicKeysToSync[relayURL]?.add(
giftWrapMessage.receiverPublicKey
)
}
}
database.giftWrapMessageDao().upsert(
giftWrapMessage
)
// TODO: Might want to process this somewhere else where privateKey is available...
SeedManager.activeKeyPair().privKey?.let { activeUserPrivateKey ->
giftWrapMessage.decryptGiftWrapSeal(
activeUserPrivateKey
)?.let { giftWrapSeal ->
database.giftWrapSealDao().upsert(
giftWrapSeal
)
giftWrapSeal.decryptGiftWrapPayload(
activeUserPrivateKey
)?.let { giftWrapPayload ->
database.giftWrapUnsignedDao().upsert(
giftWrapPayload
)
val chatRoomId = Musig2.aggregateKeys(
giftWrapPayload.receiverPublicKeys()
).value.toHex()
// Process chatMessage and chatRoom stuff...
// Find or create chatRoom
val chatRoom = database.chatRoomDao().findChatRoomById(
chatRoomId
)
if (chatRoom == null) {
database.chatRoomDao().upsert(
ChatRoom(
id = chatRoomId,
userPublicKey = SeedManager.activeKeyPair().pubKey.toHex(),
subject = null, // TODO: Get subject from tags...
createdAt = giftWrapPayload.createdAt,
initialGiftWrapUnsignedId = giftWrapPayload.id
)
)
} else {
// TODO: Update subject from tags...
}
// Save chatMessage
database.chatMessageDao().upsert(
ChatMessage(
id = giftWrapPayload.id,
senderPublicKey = giftWrapPayload.publicKey,
isUserMessage = SeedManager.activePublicKey().toHex() == giftWrapPayload.publicKey,
chatRoomId = chatRoomId,
createdAt = giftWrapPayload.createdAt,
content = giftWrapPayload.content,
)
)
}
}
}
}
nostrEvent.toPost()?.let { post -> nostrEvent.toPost()?.let { post ->
if (post.replyToId != null) { if (post.replyToId != null) {
// Find or create placeHolder note that is replyTo // Find or create placeHolder note that is replyTo
@@ -334,7 +487,7 @@ abstract class NostrDao(
if (quotedProfilePublicKey != null && quotedProfile == null) { if (quotedProfilePublicKey != null && quotedProfile == null) {
database.profileDao().insertPlaceholderProfile( database.profileDao().insertPlaceholderProfile(
Profile( Profile(
displayName = "LOADING PROFILE...", displayName = "LOADING...",
publicKey = quotedProfilePublicKey, publicKey = quotedProfilePublicKey,
createdAt = GENESIS_AT, createdAt = GENESIS_AT,
nostrEventId = nostrEvent.id, // Will get overwriting by sync, nostrEventId = nostrEvent.id, // Will get overwriting by sync,
@@ -349,52 +502,6 @@ abstract class NostrDao(
) )
) )
} }
nostrEvent.toRepostedRelation()?.let { repostedRelation ->
// Find or create placeHolder note that is being reposted
val repostedNostrEvent = database.nostrEventDao().getNostrEventById(repostedRelation.repostedNostrEventId)
val repostedProfilePublicKey = if (repostedNostrEvent == null) {
// Sync event...
if (eventIdsToSync[relayURL] == null) {
eventIdsToSync[relayURL] = mutableSetOf()
}
eventIdsToSync[relayURL]?.add(repostedRelation.repostedNostrEventId)
database.nostrEventDao().insert(
NostrEvent(
id = repostedRelation.repostedNostrEventId,
content = "Pending Sync",
createdAt = GENESIS_AT,
pubKey = nostrEvent.pubKey,
sig = "",
kind = nostrEvent.kind,
tags = emptyArray()
)
)
repostedRelation.repostedProfilePublicKey
} else {
repostedRelation.repostedProfilePublicKey ?: repostedNostrEvent.pubKey
}
val repostedProfile = repostedProfilePublicKey?.let { database.profileDao().getProfileByPublicKey(it) }
if (repostedProfilePublicKey != null && repostedProfile == null) {
database.profileDao().insertPlaceholderProfile(
Profile(
displayName = "LOADING PROFILE...",
publicKey = repostedProfilePublicKey,
createdAt = GENESIS_AT,
nostrEventId = nostrEvent.id, // Will get overwriting by sync,
)
)
}
database.repostedRelationDao().upsert(
repostedRelation.copy(
repostedProfilePublicKey = repostedProfilePublicKey
)
)
}
nostrEvent.toInReplyToRelation()?.let { inReplyToRelation -> nostrEvent.toInReplyToRelation()?.let { inReplyToRelation ->
@@ -442,7 +549,7 @@ abstract class NostrDao(
if (inReplyToRootProfilePublicKey != null && inReplyToProfile == null) { if (inReplyToRootProfilePublicKey != null && inReplyToProfile == null) {
database.profileDao().insertPlaceholderProfile( database.profileDao().insertPlaceholderProfile(
Profile( Profile(
displayName = "LOADING PROFILE...", displayName = "LOADING...",
publicKey = inReplyToRootProfilePublicKey, publicKey = inReplyToRootProfilePublicKey,
createdAt = GENESIS_AT, createdAt = GENESIS_AT,
nostrEventId = nostrEvent.id, // Will get overwriting by sync, nostrEventId = nostrEvent.id, // Will get overwriting by sync,
@@ -502,7 +609,7 @@ abstract class NostrDao(
if (profile == null) { if (profile == null) {
val placeHolderProfile = Profile( val placeHolderProfile = Profile(
displayName = "LOADING PROFILE...", displayName = "LOADING...",
publicKey = followedHexKEys, publicKey = followedHexKEys,
createdAt = GENESIS_AT, createdAt = GENESIS_AT,
nostrEventId = nostrEvent.id, // Will get overwriting by sync, nostrEventId = nostrEvent.id, // Will get overwriting by sync,

View File

@@ -8,6 +8,7 @@ import androidx.room3.Entity
import androidx.room3.ForeignKey import androidx.room3.ForeignKey
import androidx.room3.PrimaryKey import androidx.room3.PrimaryKey
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlin.time.Clock
import kotlin.time.Instant import kotlin.time.Instant
@Entity( @Entity(
@@ -19,9 +20,9 @@ import kotlin.time.Instant
onDelete = ForeignKey.CASCADE, onDelete = ForeignKey.CASCADE,
), ),
ForeignKey( ForeignKey(
entity = GiftWrapUnsigned::class, entity = GiftWrapPayload::class,
parentColumns = ["id"], parentColumns = ["id"],
childColumns = ["giftWrapUnsignedId"], childColumns = ["id"],
onDelete = ForeignKey.CASCADE, onDelete = ForeignKey.CASCADE,
), ),
], ],
@@ -35,17 +36,15 @@ data class ChatMessage(
val chatRoomId: String, val chatRoomId: String,
val giftWrapUnsignedId: String, val replyToEventId: String? = null,
val quotedEventId: String? = null,
val replyToEventId: String?,
val quotedEventId: String?,
val content: String, val content: String,
override val createdAt: Instant, override val createdAt: Instant = Clock.System.now(),
override val updatedAt: Instant, override val updatedAt: Instant = createdAt,
override val savedAt: Instant, override val savedAt: Instant = Clock.System.now(),
override val viewedAt: Instant?, override val viewedAt: Instant? = null,
override val deletedAt: Instant?, // Who this message should be seen by (used to derive conversationId) override val deletedAt: Instant? = null, // Who this message should be seen by (used to derive conversationId)
): TimestampedEntity, LocalStoreEntity, UserViewableEntity, SoftDeletableEntity { ): TimestampedEntity, LocalStoreEntity, UserViewableEntity, SoftDeletableEntity {
} }

View File

@@ -20,7 +20,7 @@ import kotlin.time.Instant
onDelete = ForeignKey.CASCADE, onDelete = ForeignKey.CASCADE,
), ),
ForeignKey( ForeignKey(
entity = GiftWrapUnsigned::class, entity = GiftWrapPayload::class,
parentColumns = ["id"], parentColumns = ["id"],
childColumns = ["initialGiftWrapUnsignedId"], childColumns = ["initialGiftWrapUnsignedId"],
onDelete = ForeignKey.CASCADE, onDelete = ForeignKey.CASCADE,
@@ -50,11 +50,6 @@ data class ChatRoom(
*/ */
val subject: String?, val subject: String?,
/**
* Should probably be the human-readable name of whichever participant is not the user...
*/
val name: String?,
/** /**
* Derived from kind14/15 tags * Derived from kind14/15 tags
* [ * [

View File

@@ -11,6 +11,15 @@ import androidx.room3.PrimaryKey
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import fr.acinq.bitcoin.PrivateKey
import kotlin.time.Clock
import kotlin.time.Instant import kotlin.time.Instant
@Entity( @Entity(
@@ -33,12 +42,19 @@ data class GiftWrapMessage(
val publicKey: String, val publicKey: String,
val kind: Kind = 1059, val kind: Kind = 1059,
/** /**
* Receiver Public Key Derived from tags
*
* [ * [
* ["p", receiverPublicKey, "<relay-url>"] // receiver * ["p", receiverPublicKey, "<relay-url>"] // receiver
* ] * ]
*
*/ */
val tags: Array<Array<String>>, val receiverPublicKey: String,
val receiverRelayHit: String?,
/** /**
* *
* encrypt(GiftWrapSeal, randomPrivateKey, receiverPublicKey) * encrypt(GiftWrapSeal, randomPrivateKey, receiverPublicKey)
@@ -57,47 +73,50 @@ data class GiftWrapMessage(
/** /**
* randomTimeUpTo2DaysInThePast * randomTimeUpTo2DaysInThePast
*/ */
override val createdAt: Instant, override val createdAt: Instant = Clock.System.now(),
override val updatedAt: Instant, override val updatedAt: Instant = createdAt,
override val savedAt: Instant, override val savedAt: Instant = Clock.System.now(),
override val viewedAt: Instant?, override val viewedAt: Instant? = null,
override val deletedAt: Instant?, // Who this message should be seen by (used to derive conversationId) override val deletedAt: Instant? = null, // Who this message should be seen by (used to derive conversationId)
): NostrEventEntity, TimestampedEntity, LocalStoreEntity, UserViewableEntity, SoftDeletableEntity { ): NostrEventEntity, TimestampedEntity, LocalStoreEntity, UserViewableEntity, SoftDeletableEntity {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as GiftWrapMessage suspend fun decryptGiftWrapSeal(
privateKey: ByteArray
): GiftWrapSeal? {
if (kind == GiftWrapEvent.KIND) {
val giftWrapEvent = GiftWrapEvent(
id = id,
pubKey = publicKey,
tags = arrayOf(
PTag(
pubKey = receiverPublicKey,
relayHint = receiverRelayHit?.let { NormalizedRelayUrl(it) }
).toTagArray()
),
createdAt = createdAt.epochSeconds,
content = content,
sig = signature
)
if (kind != other.kind) return false val nostrSigner = NostrSignerInternal(
if (id != other.id) return false keyPair = KeyPair(privKey = privateKey)
if (publicKey != other.publicKey) return false )
if (!tags.contentDeepEquals(other.tags)) return false
if (content != other.content) return false
if (signature != other.signature) return false
if (nostrEventId != other.nostrEventId) return false
if (createdAt != other.createdAt) return false
if (updatedAt != other.updatedAt) return false
if (savedAt != other.savedAt) return false
if (viewedAt != other.viewedAt) return false
if (deletedAt != other.deletedAt) return false
return true giftWrapEvent.unwrapOrNull(
} nostrSigner
)?.let { giftWrapSeal ->
return GiftWrapSeal(
id = giftWrapSeal.id,
publicKey = giftWrapSeal.pubKey,
content = giftWrapSeal.content,
tags = giftWrapSeal.tags,
createdAt = Instant.fromEpochSeconds(giftWrapSeal.createdAt),
signature = giftWrapSeal.sig
)
}
}
override fun hashCode(): Int {
var result = kind return null
result = 31 * result + id.hashCode()
result = 31 * result + publicKey.hashCode()
result = 31 * result + tags.contentDeepHashCode()
result = 31 * result + content.hashCode()
result = 31 * result + signature.hashCode()
result = 31 * result + nostrEventId.hashCode()
result = 31 * result + createdAt.hashCode()
result = 31 * result + updatedAt.hashCode()
result = 31 * result + savedAt.hashCode()
result = 31 * result + (viewedAt?.hashCode() ?: 0)
result = 31 * result + (deletedAt?.hashCode() ?: 0)
return result
} }
} }

View File

@@ -9,6 +9,9 @@ import androidx.room3.ForeignKey
import androidx.room3.PrimaryKey import androidx.room3.PrimaryKey
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
import fr.acinq.bitcoin.PublicKey
import kotlin.time.Clock
import kotlin.time.Instant import kotlin.time.Instant
@Entity( @Entity(
@@ -21,7 +24,7 @@ import kotlin.time.Instant
), ),
], ],
) )
data class GiftWrapUnsigned( data class GiftWrapPayload( // TODO: Rename this to GiftWrapPayload...
@PrimaryKey @PrimaryKey
val id: HexKey, val id: HexKey,
@@ -31,9 +34,9 @@ data class GiftWrapUnsigned(
val publicKey: String, val publicKey: String,
/** /**
* Chat Message Kind * Payload Event Kind
*/ */
val kind: Kind = 14, val kind: Kind,
/** /**
* Tags for the unsigned giftwrap * Tags for the unsigned giftwrap
@@ -54,7 +57,7 @@ data class GiftWrapUnsigned(
* content MUST be plain text. Fields id and created_at are required. * content MUST be plain text. Fields id and created_at are required.
*/ */
val content: String, val content: String,
val quotedEventId: String?, val quotedEventId: String? = null,
/** /**
* Associated GiftWrapSeal * Associated GiftWrapSeal
@@ -64,17 +67,28 @@ data class GiftWrapUnsigned(
/** /**
* Current time * Current time
*/ */
override val createdAt: Instant, override val createdAt: Instant = Clock.System.now(),
override val updatedAt: Instant, override val updatedAt: Instant = createdAt,
override val savedAt: Instant, override val savedAt: Instant = Clock.System.now(),
override val viewedAt: Instant?, override val viewedAt: Instant? = null,
override val deletedAt: Instant?, // Who this message should be seen by (used to derive conversationId) override val deletedAt: Instant? = null, // Who this message should be seen by (used to derive conversationId)
): TimestampedEntity, LocalStoreEntity, UserViewableEntity, SoftDeletableEntity { ): TimestampedEntity, LocalStoreEntity, UserViewableEntity, SoftDeletableEntity {
fun receiverPublicKeys(): List<PublicKey> = try {
tags.taggedUsers().map {
PublicKey(
it.pubKey.hexToByteArray()
)
}
} catch (e: Throwable) {
return emptyList()
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other == null || this::class != other::class) return false if (other == null || this::class != other::class) return false
other as GiftWrapUnsigned other as GiftWrapPayload
if (kind != other.kind) return false if (kind != other.kind) return false
if (id != other.id) return false if (id != other.id) return false

View File

@@ -9,7 +9,10 @@ import androidx.room3.ForeignKey
import androidx.room3.PrimaryKey import androidx.room3.PrimaryKey
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
import kotlin.time.Clock
import kotlin.time.Instant import kotlin.time.Instant
@Entity( @Entity(
@@ -56,12 +59,48 @@ data class GiftWrapSeal(
/** /**
* randomTimeUpTo2DaysInThePast() * randomTimeUpTo2DaysInThePast()
*/ */
override val createdAt: Instant, override val createdAt: Instant = Clock.System.now(),
override val updatedAt: Instant, override val updatedAt: Instant = createdAt,
override val savedAt: Instant, override val savedAt: Instant = Clock.System.now(),
override val viewedAt: Instant?, override val viewedAt: Instant? = null,
override val deletedAt: Instant?, // Who this message should be seen by (used to derive conversationId) override val deletedAt: Instant? = null, // Who this message should be seen by (used to derive conversationId)
): TimestampedEntity, LocalStoreEntity, UserViewableEntity, SoftDeletableEntity { ): TimestampedEntity, LocalStoreEntity, UserViewableEntity, SoftDeletableEntity {
suspend fun decryptGiftWrapPayload(
privateKey: ByteArray
): GiftWrapPayload? {
if (kind == SealedRumorEvent.KIND) {
val sealedRumorEvent = SealedRumorEvent(
id = id,
pubKey = publicKey,
tags = tags,
createdAt = createdAt.epochSeconds,
content = content,
sig = signature
)
val nostrSigner = NostrSignerInternal(
keyPair = KeyPair(privKey = privateKey)
)
sealedRumorEvent.unsealOrNull(
nostrSigner
)?.let { giftWrapPayload ->
return GiftWrapPayload(
id = giftWrapPayload.id,
publicKey = giftWrapPayload.pubKey,
kind = giftWrapPayload.kind,
tags = giftWrapPayload.tags,
content = giftWrapPayload.content,
createdAt = Instant.fromEpochSeconds(giftWrapPayload.createdAt),
)
}
}
return null
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other == null || this::class != other::class) return false if (other == null || this::class != other::class) return false

View File

@@ -5,7 +5,6 @@ import ac.cord.auxiliary.compose.database.model.traits.LocalStoreEntity
import ac.cord.auxiliary.compose.database.model.traits.SoftDeletableEntity import ac.cord.auxiliary.compose.database.model.traits.SoftDeletableEntity
import ac.cord.auxiliary.compose.database.model.traits.TimestampedEntity import ac.cord.auxiliary.compose.database.model.traits.TimestampedEntity
import ac.cord.auxiliary.compose.database.model.traits.UnsignedNostrEventEntity import ac.cord.auxiliary.compose.database.model.traits.UnsignedNostrEventEntity
import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
import ac.cord.auxiliary.compose.ui.composable.widgets.content.ContentParser import ac.cord.auxiliary.compose.ui.composable.widgets.content.ContentParser
import ac.cord.auxiliary.compose.ui.composable.widgets.content.ContentSegment import ac.cord.auxiliary.compose.ui.composable.widgets.content.ContentSegment
import androidx.room3.Entity import androidx.room3.Entity
@@ -16,10 +15,10 @@ import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.tags.events.firstTaggedEvent import com.vitorpamplona.quartz.nip01Core.tags.events.firstTaggedEvent
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip18Reposts.quotes.QAddressableTag import com.vitorpamplona.quartz.nip18Reposts.quotes.QAddressableTag
@@ -27,6 +26,7 @@ import com.vitorpamplona.quartz.nip18Reposts.quotes.QEventTag
import com.vitorpamplona.quartz.nip18Reposts.quotes.firstTaggedQuote import com.vitorpamplona.quartz.nip18Reposts.quotes.firstTaggedQuote
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.utils.EventFactory import com.vitorpamplona.quartz.utils.EventFactory
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
@@ -180,6 +180,40 @@ data class NostrEvent(
return null return null
} }
fun toGiftWrapMessageWithReceiverPTag(): GiftWrapMessage? = try {
if (kind == GiftWrapEvent.KIND) {
val giftWrapEvent = GiftWrapEvent(
id = id,
pubKey = pubKey,
createdAt = createdAt.epochSeconds,
kind = kind,
tags = tags,
content = content,
sig = sig
)
return giftWrapEvent.taggedUsers().firstOrNull()?.let { receiverPTag ->
GiftWrapMessage(
id = giftWrapEvent.id,
kind = giftWrapEvent.kind,
publicKey = giftWrapEvent.pubKey,
content = giftWrapEvent.content,
createdAt = Instant.fromEpochSeconds(giftWrapEvent.createdAt),
receiverPublicKey = receiverPTag.pubKey,
receiverRelayHit = receiverPTag.relayHint?.url,
signature = giftWrapEvent.sig,
nostrEventId = id,
)
}
}
return null
} catch (e: Throwable) {
return null
}
fun toReaction(): Reaction? = try { fun toReaction(): Reaction? = try {
if (kind == ReactionEvent.KIND) { if (kind == ReactionEvent.KIND) {
val reactionEvent = EventFactory.create<ReactionEvent>( val reactionEvent = EventFactory.create<ReactionEvent>(
@@ -212,7 +246,7 @@ data class NostrEvent(
return null return null
} }
fun toRepostedRelation(): RepostedRelation? = try { fun toRepostedRelationWithContainedPost(): Pair<RepostedRelation, Event?>? = try {
if (kind == RepostEvent.KIND) { if (kind == RepostEvent.KIND) {
val repostEvent = EventFactory.create<RepostEvent>( val repostEvent = EventFactory.create<RepostEvent>(
id = id, id = id,
@@ -224,16 +258,37 @@ data class NostrEvent(
sig = sig sig = sig
) )
return repostEvent.containedPost()?.let { containedPost -> return repostEvent.containedPost().let { containedPost ->
RepostedRelation( if (containedPost != null) {
repostedNostrEventId = containedPost.id, Pair(
repostedProfilePublicKey = containedPost.pubKey, RepostedRelation(
repostedNostrEventId = containedPost.id,
repostedProfilePublicKey = containedPost.pubKey,
repostingNostrEventId = repostEvent.id, repostingNostrEventId = repostEvent.id,
createdAt = Instant.fromEpochSeconds(repostEvent.createdAt), createdAt = Instant.fromEpochSeconds(repostEvent.createdAt),
updatedAt = Instant.fromEpochSeconds(repostEvent.createdAt), updatedAt = Instant.fromEpochSeconds(repostEvent.createdAt),
) ),
containedPost
)
} else {
repostEvent.boostedEvent()?.let { eTag ->
Pair(
RepostedRelation(
repostedNostrEventId = eTag.eventId,
repostedProfilePublicKey = eTag.author,
repostingNostrEventId = repostEvent.id,
createdAt = Instant.fromEpochSeconds(repostEvent.createdAt),
updatedAt = Instant.fromEpochSeconds(repostEvent.createdAt),
),
null
)
}
}
} }
} }

View File

@@ -0,0 +1,11 @@
package ac.cord.auxiliary.compose.database.model.intermdiate
import ac.cord.auxiliary.compose.database.model.ChatRoom
import ac.cord.auxiliary.compose.database.model.Post
import ac.cord.auxiliary.compose.database.model.Profile
import androidx.room3.Embedded
import androidx.room3.Relation
data class LocalChatRoom(
@Embedded val chatRoom: ChatRoom,
)

View File

@@ -15,6 +15,7 @@ import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest import ac.cord.auxiliary.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalNostrEvent import ac.cord.auxiliary.compose.database.model.intermdiate.LocalNostrEvent
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalAccount import ac.cord.auxiliary.compose.database.model.intermdiate.LocalAccount
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfile import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfile
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowers import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowers
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing
@@ -682,6 +683,10 @@ class DatabaseNostrRepository(
return database.profileDao().observeProfileWithFollowingByPublicKey(publicKey) return database.profileDao().observeProfileWithFollowingByPublicKey(publicKey)
} }
override suspend fun observeChatRoomsByPublicKey(publicKey: String): Flow<List<LocalChatRoom>> {
return database.chatRoomDao().observeChatRoomByUserPublicKey(publicKey)
}
override suspend fun getRecentSearches(): List<RecentSearch> { override suspend fun getRecentSearches(): List<RecentSearch> {
return database.recentSearchDao().getAllRecentSearches() return database.recentSearchDao().getAllRecentSearches()
} }

View File

@@ -12,6 +12,7 @@ import ac.cord.auxiliary.compose.database.model.UnsignedNostrEvent
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest import ac.cord.auxiliary.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalNostrEvent import ac.cord.auxiliary.compose.database.model.intermdiate.LocalNostrEvent
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalAccount import ac.cord.auxiliary.compose.database.model.intermdiate.LocalAccount
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfile import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfile
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowers import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowers
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing
@@ -122,6 +123,8 @@ interface NostrRepository {
suspend fun observeProfileWithFollowing(publicKey: String): Flow<LocalProfileWithFollowing?> suspend fun observeProfileWithFollowing(publicKey: String): Flow<LocalProfileWithFollowing?>
suspend fun observeChatRoomsByPublicKey(publicKey: String): Flow<List<LocalChatRoom>>
suspend fun getRecentSearches(): List<RecentSearch> suspend fun getRecentSearches(): List<RecentSearch>
suspend fun removeRecentSearch(recentSearch: RecentSearch) suspend fun removeRecentSearch(recentSearch: RecentSearch)
@@ -289,6 +292,10 @@ interface NostrRepository {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun observeChatRoomsByPublicKey(publicKey: String): Flow<List<LocalChatRoom>> {
TODO("Not yet implemented")
}
override suspend fun getRecentSearches(): List<RecentSearch> { override suspend fun getRecentSearches(): List<RecentSearch> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }

View File

@@ -8,7 +8,6 @@ import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.view.state.ChatRoomListUIState import ac.cord.auxiliary.compose.ui.view.state.ChatRoomListUIState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -49,51 +48,20 @@ class ChatRoomListViewModel(
var chatRoomListUIState: ChatRoomListUIState by mutableStateOf(initialChatRoomListUIState) var chatRoomListUIState: ChatRoomListUIState by mutableStateOf(initialChatRoomListUIState)
private set private set
val isSynchronizationPending: MutableState<Boolean> = mutableStateOf(false)
fun initiate() { fun initiate() {
logger.d("init") logger.d("init")
// scheduleSynchronization() scheduleSynchronization()
observeLocalNostrFeed() observeChatRoomFeed()
// observeSynchronizeNostrEventRequestByPurposeAndStatusCount()
} }
fun observeLocalNostrFeed() {
logger.d("observeLocalNostrFeed") fun observeChatRoomFeed() {
logger.d("observeChatRoomFeed")
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
nostrRepository.observeProfileWithFollowing( nostrRepository.observeChatRoomsByPublicKey(
publicKey publicKey
).distinctUntilChanged().collect { ).distinctUntilChanged().collect {
chatRoomListUIState = ChatRoomListUIState.Loaded( chatRoomListUIState = ChatRoomListUIState.Loaded(
chatRoomList = listOf( chatRoomList = it
ChatRoom(
id ="steveBikoId",
userPublicKey = "steveBikoId",
name = "Steve Biko",
subject = "BC: Systems",
initialGiftWrapUnsignedId = "",
),
ChatRoom(
id = "chrisHaniId",
userPublicKey = "chrisHaniId",
name = "Chris Hani",
subject = "MK: Gym",
initialGiftWrapUnsignedId = "",
),
ChatRoom(
id = "desmondTutuId",
userPublicKey = "desmondTutuId",
name = "Desmond Tutu",
subject = "TRC: Masses",
initialGiftWrapUnsignedId = "",
),
ChatRoom(
id = "hipHopPantsulaId",
userPublicKey = "hipHopPantsulaId",
name = "Hip Hop Pantsula",
subject = "Motswako: Jabba",
initialGiftWrapUnsignedId = "",
)
)
) )
} }
} }
@@ -110,7 +78,7 @@ class ChatRoomListViewModel(
relayURL = normalizedRelayUrl.url, relayURL = normalizedRelayUrl.url,
synchronizationFilter = synchronizationFilter synchronizationFilter = synchronizationFilter
), ),
purpose = "following", purpose = "chat",
synchronizationFilter = synchronizationFilter, synchronizationFilter = synchronizationFilter,
relayURL = normalizedRelayUrl.url, relayURL = normalizedRelayUrl.url,
level = 0 level = 0
@@ -120,16 +88,6 @@ class ChatRoomListViewModel(
} }
} }
fun observeSynchronizeNostrEventRequestByPurposeAndStatusCount() {
logger.d("observeSynchronizeNostrEventRequestByPurposeAndStatusCount")
viewModelScope.launch(Dispatchers.IO) {
nostrRepository.observeSynchronizeNostrEventRequestByPurposeAndStatusCount("feed").distinctUntilChanged().collect { count ->
logger.d("Pending/Processing Synchronization Request: $count")
isSynchronizationPending.value = count > 0
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun RenderFeed( fun RenderFeed(
@@ -177,8 +135,8 @@ class ChatRoomListViewModel(
items( items(
items = chatRoomListUIState.chatRoomList, items = chatRoomListUIState.chatRoomList,
key = { it.id } key = { it.chatRoom.id }
) { chatRoom -> ) { localChatRoom ->
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onClick = { onClick = {
@@ -190,16 +148,13 @@ class ChatRoomListViewModel(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Text( Text(
text = chatRoom.name ?: chatRoom.subject ?: "Unnamed chat" text = localChatRoom.chatRoom.subject ?: "Unnamed chat"
) )
} }
} }
} }
} }
} }
} }
} }
ChatRoomListUIState.Loading -> { ChatRoomListUIState.Loading -> {
@@ -226,7 +181,7 @@ class ChatRoomListViewModel(
} }
companion object { companion object {
const val TAG = "MessageListViewModel" const val TAG = "ChatRoomListViewModel"
fun factory( fun factory(
initialChatRoomListUIState: ChatRoomListUIState = ChatRoomListUIState.Loading, initialChatRoomListUIState: ChatRoomListUIState = ChatRoomListUIState.Loading,

View File

@@ -23,6 +23,7 @@ import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
@@ -116,11 +117,11 @@ class HomeViewModel(
} }
HomeScreenType.Messages -> { HomeScreenType.Messages -> {
SynchronizationFilter( SynchronizationFilter(
authors = arrayOf(
publicKey
),
kinds = arrayOf( kinds = arrayOf(
ContactListEvent.KIND, GiftWrapEvent.KIND,
),
tags = mapOf(
Pair("p", listOf(publicKey))
), ),
limit = 50 limit = 50
) )

View File

@@ -1,10 +1,10 @@
package ac.cord.auxiliary.compose.ui.view.state package ac.cord.auxiliary.compose.ui.view.state
import ac.cord.auxiliary.compose.database.model.ChatRoom import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
sealed interface ChatRoomListUIState { sealed interface ChatRoomListUIState {
data class Loaded( data class Loaded(
val chatRoomList: List<ChatRoom> val chatRoomList: List<LocalChatRoom>
): ChatRoomListUIState ): ChatRoomListUIState
data object Error: ChatRoomListUIState data object Error: ChatRoomListUIState