diff --git a/build.gradle.kts b/build.gradle.kts index 98ddb8c6..8ad26e5f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,4 +7,5 @@ plugins { alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeCompiler) apply false alias(libs.plugins.kotlinMultiplatform) apply false + alias(libs.plugins.ksp) apply false } \ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 7a4816dd..0e635bd6 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -2,12 +2,14 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidApplication) + alias(libs.plugins.androidx.room) + alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) alias(libs.plugins.composeHotReload) alias(libs.plugins.kotlinPluginSerialization) + alias(libs.plugins.ksp) } kotlin { @@ -34,6 +36,7 @@ kotlin { implementation(libs.compose.uiToolingPreview) implementation(libs.androidx.activity.compose) implementation(libs.okhttp.coroutines) + implementation(libs.androidx.room.sqlite.wrapper) } commonMain.dependencies { implementation(libs.compose.runtime) @@ -47,6 +50,11 @@ kotlin { implementation(libs.androidx.lifecycle.viewmodelCompose) implementation(libs.androidx.lifecycle.runtimeCompose) + implementation("androidx.paging:paging-common:3.5.0-alpha01") + implementation("androidx.paging:paging-compose:3.5.0-alpha01") + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.sqlite.bundled) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0") implementation(libs.navigation.compose) @@ -98,6 +106,16 @@ android { dependencies { debugImplementation(libs.compose.uiTooling) + add("kspAndroid", libs.androidx.room.compiler) + add("kspIosSimulatorArm64", libs.androidx.room.compiler) +// add("kspIosX64", libs.androidx.room.compiler) + add("kspIosArm64", libs.androidx.room.compiler) + add("kspJvm", libs.androidx.room.compiler) + // Add any other platform target you use in your project, for example kspDesktop +} + +room { + schemaDirectory("$projectDir/schemas") } compose.desktop { diff --git a/composeApp/schemas/ac.aux.compose.database.AuxDatabase/1.json b/composeApp/schemas/ac.aux.compose.database.AuxDatabase/1.json new file mode 100644 index 00000000..acefeedb --- /dev/null +++ b/composeApp/schemas/ac.aux.compose.database.AuxDatabase/1.json @@ -0,0 +1,701 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "9a4e13a505370c8fd3433ef4241cbcc8", + "entities": [ + { + "tableName": "NostrEvent", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `pubKey` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `broadcastedAt` INTEGER NOT NULL, `kind` INTEGER NOT NULL, `tags` TEXT NOT NULL, `content` TEXT NOT NULL, `sig` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubKey", + "columnName": "pubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "savedAt", + "columnName": "savedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "broadcastedAt", + "columnName": "broadcastedAt", + "affinity": "INTEGER", + "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": "sig", + "columnName": "sig", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "Post", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `nostrEventId` TEXT NOT NULL, `replyToId` TEXT, `repostId` TEXT, `quote` TEXT, `profilePublicKey` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `broadcastedAt` INTEGER NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`profilePublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`nostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`replyToId`) REFERENCES `Post`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`repostId`) REFERENCES `Post`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nostrEventId", + "columnName": "nostrEventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replyToId", + "columnName": "replyToId", + "affinity": "TEXT" + }, + { + "fieldPath": "repostId", + "columnName": "repostId", + "affinity": "TEXT" + }, + { + "fieldPath": "quote", + "columnName": "quote", + "affinity": "TEXT" + }, + { + "fieldPath": "profilePublicKey", + "columnName": "profilePublicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "savedAt", + "columnName": "savedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "broadcastedAt", + "columnName": "broadcastedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Post_profilePublicKey", + "unique": false, + "columnNames": [ + "profilePublicKey" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Post_profilePublicKey` ON `${TABLE_NAME}` (`profilePublicKey`)" + }, + { + "name": "index_Post_nostrEventId", + "unique": false, + "columnNames": [ + "nostrEventId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Post_nostrEventId` ON `${TABLE_NAME}` (`nostrEventId`)" + }, + { + "name": "index_Post_replyToId", + "unique": false, + "columnNames": [ + "replyToId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Post_replyToId` ON `${TABLE_NAME}` (`replyToId`)" + }, + { + "name": "index_Post_repostId", + "unique": false, + "columnNames": [ + "repostId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Post_repostId` ON `${TABLE_NAME}` (`repostId`)" + } + ], + "foreignKeys": [ + { + "table": "Profile", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "profilePublicKey" + ], + "referencedColumns": [ + "publicKey" + ] + }, + { + "table": "NostrEvent", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "nostrEventId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Post", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "replyToId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Post", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repostId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Profile", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`publicKey` TEXT NOT NULL, `nostrEventId` TEXT NOT NULL, `userName` TEXT, `displayName` TEXT, `picture` TEXT, `banner` TEXT, `website` TEXT, `about` TEXT, `bot` INTEGER, `pronouns` TEXT, `nip05` TEXT, `domain` TEXT, `lud06` TEXT, `lud16` TEXT, `twitter` TEXT, PRIMARY KEY(`publicKey`), FOREIGN KEY(`nostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nostrEventId", + "columnName": "nostrEventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "picture", + "columnName": "picture", + "affinity": "TEXT" + }, + { + "fieldPath": "banner", + "columnName": "banner", + "affinity": "TEXT" + }, + { + "fieldPath": "website", + "columnName": "website", + "affinity": "TEXT" + }, + { + "fieldPath": "about", + "columnName": "about", + "affinity": "TEXT" + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER" + }, + { + "fieldPath": "pronouns", + "columnName": "pronouns", + "affinity": "TEXT" + }, + { + "fieldPath": "nip05", + "columnName": "nip05", + "affinity": "TEXT" + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT" + }, + { + "fieldPath": "lud06", + "columnName": "lud06", + "affinity": "TEXT" + }, + { + "fieldPath": "lud16", + "columnName": "lud16", + "affinity": "TEXT" + }, + { + "fieldPath": "twitter", + "columnName": "twitter", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "publicKey" + ] + }, + "indices": [ + { + "name": "index_Profile_nostrEventId", + "unique": false, + "columnNames": [ + "nostrEventId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Profile_nostrEventId` ON `${TABLE_NAME}` (`nostrEventId`)" + } + ], + "foreignKeys": [ + { + "table": "NostrEvent", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "nostrEventId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Reaction", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `nostrEventId` TEXT NOT NULL, `replyToId` TEXT NOT NULL, `profilePublicKey` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`profilePublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`nostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`replyToId`) REFERENCES `Post`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nostrEventId", + "columnName": "nostrEventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replyToId", + "columnName": "replyToId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePublicKey", + "columnName": "profilePublicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Reaction_profilePublicKey", + "unique": false, + "columnNames": [ + "profilePublicKey" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Reaction_profilePublicKey` ON `${TABLE_NAME}` (`profilePublicKey`)" + }, + { + "name": "index_Reaction_nostrEventId", + "unique": false, + "columnNames": [ + "nostrEventId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Reaction_nostrEventId` ON `${TABLE_NAME}` (`nostrEventId`)" + }, + { + "name": "index_Reaction_replyToId", + "unique": false, + "columnNames": [ + "replyToId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Reaction_replyToId` ON `${TABLE_NAME}` (`replyToId`)" + } + ], + "foreignKeys": [ + { + "table": "Profile", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "profilePublicKey" + ], + "referencedColumns": [ + "publicKey" + ] + }, + { + "table": "NostrEvent", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "nostrEventId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Post", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "replyToId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Repost", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `nostrEventId` TEXT NOT NULL, `postId` TEXT, `profilePublicKey` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`profilePublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`nostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`postId`) REFERENCES `Post`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nostrEventId", + "columnName": "nostrEventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "postId", + "columnName": "postId", + "affinity": "TEXT" + }, + { + "fieldPath": "profilePublicKey", + "columnName": "profilePublicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Repost_profilePublicKey", + "unique": false, + "columnNames": [ + "profilePublicKey" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Repost_profilePublicKey` ON `${TABLE_NAME}` (`profilePublicKey`)" + }, + { + "name": "index_Repost_nostrEventId", + "unique": false, + "columnNames": [ + "nostrEventId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Repost_nostrEventId` ON `${TABLE_NAME}` (`nostrEventId`)" + }, + { + "name": "index_Repost_postId", + "unique": false, + "columnNames": [ + "postId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Repost_postId` ON `${TABLE_NAME}` (`postId`)" + } + ], + "foreignKeys": [ + { + "table": "Profile", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "profilePublicKey" + ], + "referencedColumns": [ + "publicKey" + ] + }, + { + "table": "NostrEvent", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "nostrEventId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Post", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "postId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Zap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `nostrEventId` TEXT NOT NULL, `postId` TEXT NOT NULL, `senderPublicKey` TEXT NOT NULL, `receiverPublicKey` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `message` TEXT, `invoice` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`senderPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`receiverPublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`nostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`postId`) REFERENCES `Post`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nostrEventId", + "columnName": "nostrEventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "postId", + "columnName": "postId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderPublicKey", + "columnName": "senderPublicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "receiverPublicKey", + "columnName": "receiverPublicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT" + }, + { + "fieldPath": "invoice", + "columnName": "invoice", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Zap_senderPublicKey", + "unique": false, + "columnNames": [ + "senderPublicKey" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Zap_senderPublicKey` ON `${TABLE_NAME}` (`senderPublicKey`)" + }, + { + "name": "index_Zap_nostrEventId", + "unique": false, + "columnNames": [ + "nostrEventId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Zap_nostrEventId` ON `${TABLE_NAME}` (`nostrEventId`)" + }, + { + "name": "index_Zap_postId", + "unique": false, + "columnNames": [ + "postId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Zap_postId` ON `${TABLE_NAME}` (`postId`)" + } + ], + "foreignKeys": [ + { + "table": "Profile", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "senderPublicKey" + ], + "referencedColumns": [ + "publicKey" + ] + }, + { + "table": "Profile", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "receiverPublicKey" + ], + "referencedColumns": [ + "publicKey" + ] + }, + { + "table": "NostrEvent", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "nostrEventId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Post", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "postId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "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, '9a4e13a505370c8fd3433ef4241cbcc8')" + ] + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/AuxDatabase.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/AuxDatabase.kt new file mode 100644 index 00000000..250d6f24 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/AuxDatabase.kt @@ -0,0 +1,42 @@ +package ac.aux.compose.database + +import ac.aux.compose.database.converters.Converters +import ac.aux.compose.database.dao.NostrDao +import ac.aux.compose.database.dao.NostrEventDao +import ac.aux.compose.database.dao.PostDao +import ac.aux.compose.database.dao.ProfileDao +import ac.aux.compose.database.dao.ReactionDao +import ac.aux.compose.database.dao.RepostDao +import ac.aux.compose.database.dao.ZapDao +import ac.aux.compose.database.model.NostrEvent +import ac.aux.compose.database.model.Post +import ac.aux.compose.database.model.Profile +import ac.aux.compose.database.model.Reaction +import ac.aux.compose.database.model.Repost +import ac.aux.compose.database.model.Zap +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters + +@Database( + entities = [ + NostrEvent::class, + Post::class, + Profile::class, + Reaction::class, + Repost::class, + Zap::class + ], + version = 1 +) +@TypeConverters(Converters::class) +abstract class AuxDatabase: RoomDatabase() { + abstract fun nostrDao(): NostrDao + + abstract fun nostrEventDao(): NostrEventDao + abstract fun postDao(): PostDao + abstract fun profileDao(): ProfileDao + abstract fun reactionDao(): ReactionDao + abstract fun repostDao(): RepostDao + abstract fun zapDao(): ZapDao +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/converters/Converters.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/converters/Converters.kt new file mode 100644 index 00000000..083d10cb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/converters/Converters.kt @@ -0,0 +1,30 @@ +package ac.aux.compose.database.converters + +import androidx.room.TypeConverter +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import kotlinx.serialization.json.Json +import kotlin.time.Instant + +class Converters { + @TypeConverter + fun fromTimestamp(value: Long?): Instant? { + return value?.let { Instant.fromEpochMilliseconds(it) } + } + + @TypeConverter + fun instantToTimestamp(instant: Instant?): Long? { + return instant?.toEpochMilliseconds() + } + + @TypeConverter + fun fromTagArray(value: TagArray?): String? { + // Returns null if the value is null, otherwise serializes to JSON string + return value?.let { Json.encodeToString(it) } + } + + @TypeConverter + fun toTagArray(value: String?): TagArray? { + // Decodes the JSON string back into the nested array structure + return value?.let { Json.decodeFromString(it) } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt new file mode 100644 index 00000000..f8fa94cc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt @@ -0,0 +1,18 @@ +package ac.aux.compose.database.dao + +import ac.aux.compose.database.AuxDatabase +import ac.aux.compose.database.model.NostrEvent +import ac.aux.compose.database.model.Post +import androidx.room.Dao +import androidx.room.Transaction + +@Dao +abstract class NostrDao( + private val database: AuxDatabase +) { + @Transaction + open suspend fun insertPostAndNostrEvent(post: Post, nostrEvent: NostrEvent) { + database.nostrEventDao().insert(nostrEvent) + database.postDao().insert(post) // TODO: Make use of the nostrEventId... + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrEventDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrEventDao.kt new file mode 100644 index 00000000..714e6003 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrEventDao.kt @@ -0,0 +1,19 @@ +package ac.aux.compose.database.dao + +import ac.aux.compose.database.model.NostrEvent +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface NostrEventDao { + @Query("SELECT * FROM NostrEvent") + fun getAllNostrEvents(): List + + @Query("SELECT * FROM NostrEvent WHERE id = :id") + fun getNostrEventById(id: String): Flow + + @Insert + suspend fun insert(nostrEvent: NostrEvent) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/PostDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/PostDao.kt new file mode 100644 index 00000000..f3f9420c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/PostDao.kt @@ -0,0 +1,31 @@ +package ac.aux.compose.database.dao + +import ac.aux.compose.database.model.Post +import ac.aux.compose.database.model.Profile +import androidx.room.Dao +import androidx.room.Embedded +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Relation +import kotlinx.coroutines.flow.Flow + +@Dao +interface PostDao { + @Query("SELECT * FROM Post") + fun getAllPosts(): List + + @Query("SELECT * FROM Post WHERE id = :id") + fun getPostById(id: String): Flow + + @Insert + suspend fun insert(post: Post) + + data class PostAndProfile( + @Embedded val post: Post, + @Relation( + parentColumn = "profilePublicKey", + entityColumn = "publicKey" + ) + val profile: Profile + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ProfileDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ProfileDao.kt new file mode 100644 index 00000000..b4ff11cc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ProfileDao.kt @@ -0,0 +1,21 @@ +package ac.aux.compose.database.dao + +import ac.aux.compose.database.model.NostrEvent +import ac.aux.compose.database.model.Post +import ac.aux.compose.database.model.Profile +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface ProfileDao { + @Query("SELECT * FROM Profile") + fun getAllPosts(): List + + @Query("SELECT * FROM Profile WHERE publicKey = :publicKey") + fun getProfileByPublicKey(publicKey: String): Flow + + @Insert + suspend fun insert(profile: Profile) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ReactionDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ReactionDao.kt new file mode 100644 index 00000000..572d5448 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ReactionDao.kt @@ -0,0 +1,22 @@ +package ac.aux.compose.database.dao + +import ac.aux.compose.database.model.NostrEvent +import ac.aux.compose.database.model.Post +import ac.aux.compose.database.model.Profile +import ac.aux.compose.database.model.Reaction +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface ReactionDao { + @Query("SELECT * FROM Reaction") + fun getAllReactions(): List + + @Query("SELECT * FROM Reaction WHERE id = :id") + fun getReactionById(id: String): Flow + + @Insert + suspend fun insert(reaction: Reaction) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/RepostDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/RepostDao.kt new file mode 100644 index 00000000..f8c2251b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/RepostDao.kt @@ -0,0 +1,21 @@ +package ac.aux.compose.database.dao + +import ac.aux.compose.database.model.NostrEvent +import ac.aux.compose.database.model.Post +import ac.aux.compose.database.model.Repost +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface RepostDao { + @Query("SELECT * FROM Repost") + fun getAllReposts(): List + + @Query("SELECT * FROM Repost WHERE id = :id") + fun getRepostById(id: String): Flow + + @Insert + suspend fun insert(repost: Repost) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ZapDao.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ZapDao.kt new file mode 100644 index 00000000..1578bf66 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/ZapDao.kt @@ -0,0 +1,18 @@ +package ac.aux.compose.database.dao + +import ac.aux.compose.database.model.NostrEvent +import ac.aux.compose.database.model.Post +import ac.aux.compose.database.model.Zap +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface ZapDao { + @Query("SELECT * FROM Zap WHERE id = :id") + fun getZapById(id: String): Flow + + @Insert + suspend fun insert(zap: Zap) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/NostrEvent.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/NostrEvent.kt new file mode 100644 index 00000000..c800e8d3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/NostrEvent.kt @@ -0,0 +1,53 @@ +package ac.aux.compose.database.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Fts4 +import androidx.room.Index +import androidx.room.PrimaryKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Kind +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import kotlin.time.Instant + +@Entity +data class NostrEvent( + @PrimaryKey + val id: String, + val pubKey: HexKey, + val createdAt: Instant, + val savedAt: Instant, + val broadcastedAt: Instant, + val kind: Kind, + val tags: TagArray, + val content: String, + val sig: HexKey, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as NostrEvent + + if (kind != other.kind) return false + if (id != other.id) return false + if (pubKey != other.pubKey) return false + if (createdAt != other.createdAt) return false + if (!tags.contentDeepEquals(other.tags)) return false + if (content != other.content) return false + if (sig != other.sig) return false + + return true + } + + override fun hashCode(): Int { + var result = kind + result = 31 * result + id.hashCode() + result = 31 * result + pubKey.hashCode() + result = 31 * result + createdAt.hashCode() + result = 31 * result + tags.contentDeepHashCode() + result = 31 * result + content.hashCode() + result = 31 * result + sig.hashCode() + return result + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Post.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Post.kt new file mode 100644 index 00000000..e341e1c0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Post.kt @@ -0,0 +1,76 @@ +package ac.aux.compose.database.model + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Kind +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import kotlin.time.Clock +import kotlin.time.Instant + +@Entity( + foreignKeys = [ + ForeignKey( + entity = Profile::class, + parentColumns = ["publicKey"], + childColumns = ["profilePublicKey"], + onDelete = ForeignKey.CASCADE, + ), + // This might cause problems... but we gonna make the feed so cool if it works... + ForeignKey( + entity = NostrEvent::class, + parentColumns = ["id"], + childColumns = ["nostrEventId"], + onDelete = ForeignKey.CASCADE, + ), + // Support post replies + ForeignKey( + entity = Post::class, + parentColumns = ["id"], + childColumns = ["replyToId"], + onDelete = ForeignKey.CASCADE, + ), + // Support reposts + ForeignKey( + entity = Post::class, + parentColumns = ["id"], + childColumns = ["repostId"], + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [ + Index("profilePublicKey"), + Index("nostrEventId"), + Index("replyToId"), + Index("repostId") + ], +) +data class Post( + @PrimaryKey + val id: HexKey, + val nostrEventId: HexKey, // Might be a replaceable nostr event? + val replyToId: HexKey? = null, + val repostId: HexKey? = null, + val quote: HexKey? = null, + val profilePublicKey: HexKey, + val createdAt: Instant, + val savedAt: Instant, + val broadcastedAt: Instant, + val content: String, +) { + companion object { + val DUMMY_POSTS = arrayOf( + Post( + id = "genesisnoteid", + nostrEventId = "genesisnoteid", // Need to check how to handle addressableEvents... + profilePublicKey = "satoshi", + createdAt = Clock.System.now(), + savedAt = Clock.System.now(), + broadcastedAt = Clock.System.now(), + content = "Chancellor the rapper...", + ) + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Profile.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Profile.kt new file mode 100644 index 00000000..b498bd6c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Profile.kt @@ -0,0 +1,42 @@ +package ac.aux.compose.database.model + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + foreignKeys = [ + ForeignKey( + entity = NostrEvent::class, + parentColumns = ["id"], + childColumns = ["nostrEventId"], + onDelete = ForeignKey.CASCADE, + ) + ], + indices = [ + Index("nostrEventId"), + ], +) +data class Profile( + @PrimaryKey + val publicKey: String, + val nostrEventId: String, + val userName: String? = null, // name in the nostr event... + val displayName: String? = null, + val picture: String? = null, + val banner: String? = null, + val website: String? = null, + val about: String? = null, + val bot: Boolean? = null, + val pronouns: String? = null, + val nip05: String? = null, + val domain: String? = null, + val lud06: String? = null, + val lud16: String? = null, + val twitter: String? = null +) { + companion object { + val DUMMAY_PROFILES = arrayOf() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Reaction.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Reaction.kt new file mode 100644 index 00000000..b0d22778 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Reaction.kt @@ -0,0 +1,50 @@ +package ac.aux.compose.database.model + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Kind +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import kotlin.time.Clock +import kotlin.time.Instant + +@Entity( + foreignKeys = [ + ForeignKey( + entity = Profile::class, + parentColumns = ["publicKey"], + childColumns = ["profilePublicKey"], + onDelete = ForeignKey.CASCADE, + ), + // This might cause problems... but we gonna make the feed so cool if it works... + ForeignKey( + entity = NostrEvent::class, + parentColumns = ["id"], + childColumns = ["nostrEventId"], + onDelete = ForeignKey.CASCADE, + ), + // Support post replies + ForeignKey( + entity = Post::class, + parentColumns = ["id"], + childColumns = ["replyToId"], + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [ + Index("profilePublicKey"), + Index("nostrEventId"), + Index("replyToId") + ], +) +data class Reaction( + @PrimaryKey + val id: HexKey, + val nostrEventId: HexKey, // Might be a replaceable nostr event? + val replyToId: HexKey, + val profilePublicKey: HexKey, + val createdAt: Instant, + val content: String, +) diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Repost.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Repost.kt new file mode 100644 index 00000000..ebbc7f8d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Repost.kt @@ -0,0 +1,50 @@ +package ac.aux.compose.database.model + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Kind +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import kotlin.time.Clock +import kotlin.time.Instant + +@Entity( + foreignKeys = [ + ForeignKey( + entity = Profile::class, + parentColumns = ["publicKey"], + childColumns = ["profilePublicKey"], + onDelete = ForeignKey.CASCADE, + ), + // This might cause problems... but we gonna make the feed so cool if it works... + ForeignKey( + entity = NostrEvent::class, + parentColumns = ["id"], + childColumns = ["nostrEventId"], + onDelete = ForeignKey.CASCADE, + ), + // Support post replies + ForeignKey( + entity = Post::class, + parentColumns = ["id"], + childColumns = ["postId"], + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [ + Index("profilePublicKey"), + Index("nostrEventId"), + Index("postId") + ], +) +data class Repost( + @PrimaryKey + val id: HexKey, + val nostrEventId: HexKey, // Might be a replaceable nostr event? + val postId: HexKey? = null, + val profilePublicKey: HexKey, + val createdAt: Instant, + val content: String, +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Zap.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Zap.kt new file mode 100644 index 00000000..806430a4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Zap.kt @@ -0,0 +1,55 @@ +package ac.aux.compose.database.model + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import kotlin.time.Instant + +@Entity( + foreignKeys = [ + ForeignKey( + entity = Profile::class, + parentColumns = ["publicKey"], + childColumns = ["senderPublicKey"], + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = Profile::class, + parentColumns = ["publicKey"], + childColumns = ["receiverPublicKey"], + onDelete = ForeignKey.CASCADE, + ), + // This might cause problems... but we gonna make the feed so cool if it works... + ForeignKey( + entity = NostrEvent::class, + parentColumns = ["id"], + childColumns = ["nostrEventId"], + onDelete = ForeignKey.CASCADE, + ), + // Support post replies + ForeignKey( + entity = Post::class, + parentColumns = ["id"], + childColumns = ["postId"], + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [ + Index("senderPublicKey"), + Index("nostrEventId"), + Index("postId") + ], +) +data class Zap( + @PrimaryKey + val id: HexKey, + val nostrEventId: HexKey, // Might be a replaceable nostr event? + val postId: HexKey, + val senderPublicKey: HexKey, + val receiverPublicKey: HexKey, + val createdAt: Instant, + val message: String?, + val invoice: String?, +) diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabasePostRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabasePostRepository.kt new file mode 100644 index 00000000..b254a78f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabasePostRepository.kt @@ -0,0 +1,24 @@ +package ac.aux.compose.database.repository + +import ac.aux.compose.database.AuxDatabase +import ac.aux.compose.database.model.NostrEvent +import ac.aux.compose.database.model.Post +import ac.aux.compose.repository.PostRepository + +class DatabasePostRepository( + private val database: AuxDatabase +): PostRepository { + override suspend fun publishPost( + post: Post, + nostrEvent: NostrEvent + ) { + val nostrDao = database.nostrDao() + + nostrDao.insertPostAndNostrEvent( + post, + nostrEvent + ) + + + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/PostRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/PostRepository.kt new file mode 100644 index 00000000..8715dab8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/PostRepository.kt @@ -0,0 +1,8 @@ +package ac.aux.compose.repository + +import ac.aux.compose.database.model.NostrEvent +import ac.aux.compose.database.model.Post + +interface PostRepository { + suspend fun publishPost(post: Post, nostrEvent: NostrEvent) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt index e0113957..174dc55a 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/FeedScreen.kt @@ -1,11 +1,12 @@ package ac.aux.compose.ui.composable import ac.aux.compose.ui.composable.widgets.LoadingDataIndicator +import ac.aux.compose.ui.composable.widgets.feed.FeedListItem import ac.aux.compose.ui.composable.widgets.feed.ListView -import ac.aux.compose.ui.composable.widgets.feed.LoadedFeedView import ac.aux.compose.ui.theme.AuxTheme import ac.aux.compose.ui.view.model.FeedListViewModel import ac.aux.compose.ui.view.state.FeedListUIState +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -25,6 +26,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.tooling.preview.Preview @@ -91,10 +93,23 @@ fun FeedScreen( Text("Something went wrong") } is FeedListUIState.Loaded -> { - LoadedFeedView( - events = feedListUIState.events, - onNavigateToEvent = onNavigateToEvent - ) + LazyColumn( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(10.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + items( + items = feedListUIState.events, + key = { e -> e.id } + ) { event -> + FeedListItem( + event = event, + onNavigateToEvent = { + onNavigateToEvent.invoke(event) + } + ) + } + } } } } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/EventListView.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/EventListView.kt index f8dc3ec8..ca0844f3 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/EventListView.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/EventListView.kt @@ -3,6 +3,7 @@ package ac.aux.compose.ui.composable.widgets.feed import ac.aux.compose.ui.theme.AuxTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow @@ -28,6 +29,9 @@ fun Event.ListView() { verticalArrangement = Arrangement.spacedBy(10.dp), horizontalAlignment = Alignment.CenterHorizontally ) { + Row { + + } Text( text = content, ) diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/LoadedFeedView.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/LoadedFeedView.kt deleted file mode 100644 index 2fd07d05..00000000 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/LoadedFeedView.kt +++ /dev/null @@ -1,35 +0,0 @@ -package ac.aux.compose.ui.composable.widgets.feed - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.vitorpamplona.quartz.nip01Core.core.Event - -@Composable -fun LoadedFeedView( - events: List, - onNavigateToEvent: (Event) -> Unit -) { - LazyColumn( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(10.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - items( - items = events, - key = { e -> e.id } - ) { event -> - FeedListItem( - event = event, - onNavigateToEvent = { - onNavigateToEvent.invoke(event) - } - ) - } - } -} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a9675c18..fa2f80de 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,11 +15,14 @@ junit = "4.13.2" kermit = "2.0.8" kotlin = "2.3.0" kotlinx-coroutines = "1.10.2" +ksp = "2.3.6" material3 = "1.10.0-alpha05" materialIconsCore = "1.7.3" materialIconsExtended = "1.7.3" navigationCompose = "2.9.2" okhttp = "5.3.2" +room = "2.8.4" +sqlite = "2.6.2" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -30,6 +33,10 @@ androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "an androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } +androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } +androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", version.ref = "room" } compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" } androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } @@ -49,9 +56,11 @@ okhttp-coroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines" [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } +androidx-room = { id = "androidx.room", version.ref = "room" } androidLibrary = { id = "com.android.library", version.ref = "agp" } composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "composeHotReload" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } -kotlinPluginSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } \ No newline at end of file +kotlinPluginSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file