From c1d7572d4441d2ee2768019db3a1a1c76c6df5ea Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Tue, 31 Mar 2026 16:13:44 +0200 Subject: [PATCH] Handling reposts --- .../1.json | 41 +++- .../ac/aux/compose/database/dao/NostrDao.kt | 127 +++++++----- .../aux/compose/database/dao/NostrEventDao.kt | 9 +- .../ac/aux/compose/database/dao/PostDao.kt | 4 + .../aux/compose/database/model/NostrEvent.kt | 10 +- .../ac/aux/compose/database/model/Repost.kt | 55 ++++- .../model/intermdiate/LocalNostrEvent.kt | 17 ++ .../repository/DatabaseNostrRepository.kt | 15 +- .../aux/compose/repository/NostrRepository.kt | 5 +- .../aux/compose/ui/composable/FeedScreen.kt | 14 +- .../composable/widgets/feed/EventListView.kt | 193 ++++++++++++------ .../composable/widgets/feed/FeedListItem.kt | 17 +- .../ui/view/model/FeedListViewModel.kt | 34 ++- .../compose/ui/view/state/FeedListUIState.kt | 3 +- 14 files changed, 383 insertions(+), 161 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/intermdiate/LocalNostrEvent.kt diff --git a/composeApp/schemas/ac.aux.compose.database.AuxDatabase/1.json b/composeApp/schemas/ac.aux.compose.database.AuxDatabase/1.json index 2443ab2e..e7b48687 100644 --- a/composeApp/schemas/ac.aux.compose.database.AuxDatabase/1.json +++ b/composeApp/schemas/ac.aux.compose.database.AuxDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "0e3d4940d3b045a7e68c2f0b98642ab0", + "identityHash": "6c8c4b9a4b44bed3c7fde29fcf54d7df", "entities": [ { "tableName": "BroadcastNostrEventReceipt", @@ -735,7 +735,7 @@ }, { "tableName": "Repost", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `repostedPostId` TEXT NOT NULL, `content` TEXT NOT NULL, `nostrEventId` TEXT NOT NULL, `profilePublicKey` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, `broadcastedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`profilePublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`nostrEventId`) REFERENCES `NostrEvent`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`repostedPostId`) REFERENCES `Post`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `repostedPostId` TEXT NOT NULL, `repostedPostTags` TEXT NOT NULL, `repostedPostCreatedAt` INTEGER NOT NULL, `repostedPostContent` TEXT NOT NULL, `repostedPostAuthorPublicKey` TEXT NOT NULL, `content` TEXT NOT NULL, `nostrEventId` TEXT NOT NULL, `profilePublicKey` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `savedAt` INTEGER NOT NULL, `viewedAt` INTEGER, `deletedAt` INTEGER, `broadcastedAt` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`profilePublicKey`) REFERENCES `Profile`(`publicKey`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`repostedPostAuthorPublicKey`) 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(`repostedPostId`) REFERENCES `Post`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -749,6 +749,30 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "repostedPostTags", + "columnName": "repostedPostTags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repostedPostCreatedAt", + "columnName": "repostedPostCreatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repostedPostContent", + "columnName": "repostedPostContent", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repostedPostAuthorPublicKey", + "columnName": "repostedPostAuthorPublicKey", + "affinity": "TEXT", + "notNull": true + }, { "fieldPath": "content", "columnName": "content", @@ -848,6 +872,17 @@ "publicKey" ] }, + { + "table": "Profile", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repostedPostAuthorPublicKey" + ], + "referencedColumns": [ + "publicKey" + ] + }, { "table": "NostrEvent", "onDelete": "CASCADE", @@ -1307,7 +1342,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0e3d4940d3b045a7e68c2f0b98642ab0')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6c8c4b9a4b44bed3c7fde29fcf54d7df')" ] } } \ 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 index 45382c72..54d43277 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrDao.kt @@ -44,7 +44,7 @@ abstract class NostrDao( ) val placeHolderProfileNostrEvent = NostrEvent( id = placeHolderProfile.publicKey, - kind = 30024, + kind = DraftWrapEvent.KIND, pubKey = placeHolderProfile.publicKey, content = placeHolderProfile.publicKey, createdAt = Clock.System.now(), @@ -81,44 +81,6 @@ abstract class NostrDao( synchronizationRelayURLs: List ) { logger.i("Store Nostr Event: $nostrEvent") - if (nostrEvent.unsignedNostrEventId == null) { - logger.i("Store Nostr Event: $nostrEvent") - // Find or create profile with the pubKey... if not found submit a sync request... - val profile = database.profileDao().getProfileByPublicKey(nostrEvent.pubKey) - - if (profile == null) { - val placeHolderProfile = Profile( - publicKey = nostrEvent.pubKey, - nostrEventId = nostrEvent.pubKey // This is illegal... because the nostrEventId should be the one with all the profile information - ) - val placeHolderProfileNostrEvent = NostrEvent( - id = placeHolderProfile.publicKey, - kind = DraftWrapEvent.KIND, - pubKey = placeHolderProfile.publicKey, - content = placeHolderProfile.publicKey, - createdAt = Clock.System.now(), - sig = placeHolderProfile.publicKey, - tags = emptyArray(), - ) - database.nostrEventDao().upsert(placeHolderProfileNostrEvent) - database.profileDao().upsert(placeHolderProfile) - - synchronizationRelayURLs.forEach { synchronizationRelayURL -> - logger.i("Sync Profile with PubKey: ${nostrEvent.pubKey}") - database.synchronizeNostrEventRequestDao().upsert( - SynchronizeNostrEventRequest( - synchronizationFilters = arrayOf( - SynchronizationFilter( - authors = arrayOf(nostrEvent.pubKey), - kinds = arrayOf(MetadataEvent.KIND) - ) - ), - relayURL = synchronizationRelayURL - ) - ) - } - } - } database.nostrEventDao().upsert(nostrEvent) // Index nostrEvent @@ -132,6 +94,34 @@ abstract class NostrDao( nostrEvent: NostrEvent, synchronizationRelayURLs: List ) { + if (nostrEvent.unsignedNostrEventId == null) { + logger.i("Store Nostr Event: $nostrEvent") + // Find or create profile with the pubKey... if not found submit a sync request... + val profile = database.profileDao().getProfileByPublicKey(nostrEvent.pubKey) + + if (profile == null) { + val placeHolderProfile = Profile( + publicKey = nostrEvent.pubKey, + nostrEventId = nostrEvent.id, // Will get overwriting by sync, + ) + database.profileDao().insertPlaceholderProfile(placeHolderProfile) + + database.synchronizeNostrEventRequestDao().insert( + synchronizationRelayURLs.map { synchronizationRelayURL -> + SynchronizeNostrEventRequest( + synchronizationFilters = arrayOf( + SynchronizationFilter( + authors = arrayOf(nostrEvent.pubKey), + kinds = arrayOf(MetadataEvent.KIND) + ) + ), + relayURL = synchronizationRelayURL + ) + } + ) + } + } + nostrEvent.toPost()?.let { post -> if (post.replyToId != null) { // Find or create placeHolder note that is replyTo @@ -149,8 +139,8 @@ abstract class NostrDao( ) // Request a sync for the post being replied to - synchronizationRelayURLs.forEach { synchronizationRelayURL -> - database.synchronizeNostrEventRequestDao().upsert( + database.synchronizeNostrEventRequestDao().insert( + synchronizationRelayURLs.map { synchronizationRelayURL -> SynchronizeNostrEventRequest( synchronizationFilters = arrayOf( SynchronizationFilter( @@ -159,8 +149,8 @@ abstract class NostrDao( ), relayURL = synchronizationRelayURL ) - ) - } + } + ) } } @@ -178,8 +168,8 @@ abstract class NostrDao( ) ) // Request a sync for the post being reposted - synchronizationRelayURLs.forEach { synchronizationRelayURL -> - database.synchronizeNostrEventRequestDao().upsert( + database.synchronizeNostrEventRequestDao().insert( + synchronizationRelayURLs.map { synchronizationRelayURL -> SynchronizeNostrEventRequest( synchronizationFilters = arrayOf( SynchronizationFilter( @@ -188,11 +178,11 @@ abstract class NostrDao( ), relayURL = synchronizationRelayURL ) - ) - } + } + ) } } - database.postDao().insert(post) + database.postDao().upsert(post) } nostrEvent.toProfile()?.let { profile -> @@ -219,8 +209,8 @@ abstract class NostrDao( ) // Request a sync for the post being replied to - synchronizationRelayURLs.forEach { synchronizationRelayURL -> - database.synchronizeNostrEventRequestDao().upsert( + database.synchronizeNostrEventRequestDao().insert( + synchronizationRelayURLs.map { synchronizationRelayURL -> SynchronizeNostrEventRequest( synchronizationFilters = arrayOf( SynchronizationFilter( @@ -229,8 +219,35 @@ abstract class NostrDao( ), relayURL = synchronizationRelayURL ) + } + ) + } + + val repostedPostProfile = database.profileDao().getProfileByPublicKey(repost.repostedPostAuthorPublicKey) + + if (repostedPostProfile == null) { + // Persist PlaceHolder and sync + database.profileDao().insertPlaceholderProfile( + Profile( + publicKey = nostrEvent.pubKey, + nostrEventId = nostrEvent.id, // Will get overwriting by sync, ) - } + ) + + // Request a sync for the post being replied to + database.synchronizeNostrEventRequestDao().insert( + synchronizationRelayURLs.map { synchronizationRelayURL -> + SynchronizeNostrEventRequest( + synchronizationFilters = arrayOf( + SynchronizationFilter( + authors = arrayOf(nostrEvent.pubKey), + kinds = arrayOf(MetadataEvent.KIND) + ) + ), + relayURL = synchronizationRelayURL + ) + } + ) } } @@ -250,8 +267,8 @@ abstract class NostrDao( ) // Request a sync for the post being replied to - synchronizationRelayURLs.forEach { synchronizationRelayURL -> - database.synchronizeNostrEventRequestDao().upsert( + database.synchronizeNostrEventRequestDao().insert( + synchronizationRelayURLs.map { synchronizationRelayURL -> SynchronizeNostrEventRequest( synchronizationFilters = arrayOf( SynchronizationFilter( @@ -260,8 +277,8 @@ abstract class NostrDao( ), relayURL = synchronizationRelayURL ) - ) - } + } + ) } // TODO: Placeholder for all the other things... 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 index 42c68ccb..f67daaa6 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrEventDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/NostrEventDao.kt @@ -1,17 +1,22 @@ package ac.aux.compose.database.dao import ac.aux.compose.database.model.NostrEvent +import ac.aux.compose.database.model.intermdiate.LocalNostrEvent import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy.Companion.IGNORE import androidx.room.Query import androidx.room.Upsert +import com.vitorpamplona.quartz.nip01Core.core.Kind import kotlinx.coroutines.flow.Flow @Dao interface NostrEventDao { - @Query("SELECT * FROM NostrEvent ORDER BY createdAt DESC") - fun getAllNostrEvents(): List + @Query("SELECT * FROM NostrEvent WHERE kind in (:kinds) ORDER BY savedAt DESC") + fun observeNostrEvents(kinds: Array): Flow> + + @Query("SELECT * FROM NostrEvent WHERE kind in (:kinds) ORDER BY savedAt DESC") + fun getAllNostrEvents(kinds: Array): List @Query("SELECT * FROM NostrEvent WHERE id = :id") fun getNostrEventById(id: String): Flow 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 index 516d86bd..2adf3df1 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/PostDao.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/dao/PostDao.kt @@ -8,6 +8,7 @@ import androidx.room.Embedded import androidx.room.Insert import androidx.room.Query import androidx.room.Relation +import androidx.room.Upsert import kotlinx.coroutines.flow.Flow @Dao @@ -20,4 +21,7 @@ interface PostDao { @Insert suspend fun insert(post: Post) + + @Upsert + suspend fun upsert(post: Post) } \ 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 index 56afa52e..65974f8e 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/NostrEvent.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/NostrEvent.kt @@ -6,7 +6,6 @@ import ac.aux.compose.database.model.traits.SoftDeletableEntity import ac.aux.compose.database.model.traits.TimestampedEntity import ac.aux.compose.database.model.traits.UnsignedNostrEventEntity import androidx.room.Entity -import androidx.room.ForeignKey import androidx.room.Ignore import androidx.room.Index import androidx.room.PrimaryKey @@ -197,18 +196,23 @@ data class NostrEvent( sig = sig ) - return repostEvent.boostedEventId()?.let { repostedPostId -> + return repostEvent.containedPost()?.let { containedPost -> Repost( id = repostEvent.id, - repostedPostId = repostedPostId, + repostedPostId = containedPost.id, + repostedPostTags = containedPost.tags, + repostedPostContent = containedPost.content, + repostedPostCreatedAt = Instant.fromEpochSeconds(containedPost.createdAt), content = repostEvent.content, nostrEventId = id, profilePublicKey = repostEvent.pubKey, + repostedPostAuthorPublicKey = containedPost.pubKey, createdAt = Instant.fromEpochSeconds(repostEvent.createdAt), updatedAt = Instant.fromEpochSeconds(repostEvent.createdAt), ) + } } 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 index c8882035..e075f7ee 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Repost.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/Repost.kt @@ -12,6 +12,7 @@ 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.TagArray import kotlin.time.Clock import kotlin.time.Instant @@ -23,6 +24,12 @@ import kotlin.time.Instant childColumns = ["profilePublicKey"], onDelete = ForeignKey.CASCADE, ), + ForeignKey( + entity = Profile::class, + parentColumns = ["publicKey"], + childColumns = ["repostedPostAuthorPublicKey"], + onDelete = ForeignKey.CASCADE, + ), // This might cause problems... but we gonna make the feed so cool if it works... ForeignKey( entity = NostrEvent::class, @@ -47,7 +54,13 @@ import kotlin.time.Instant data class Repost( @PrimaryKey val id: HexKey, + val repostedPostId: HexKey, + val repostedPostTags: TagArray, + val repostedPostCreatedAt: Instant, + val repostedPostContent: String, + val repostedPostAuthorPublicKey: HexKey, + val content: String, override val nostrEventId: HexKey, // Might be a replaceable nostr event? @@ -59,4 +72,44 @@ data class Repost( override val viewedAt: Instant? = null, override val deletedAt: Instant? = null, override val broadcastedAt: Instant? = null, -): NostrEventEntity, ProfilePublicKeyEntity, TimestampedEntity, LocalStoreEntity, UserViewableEntity, BroadcastableEntity, SoftDeletableEntity \ No newline at end of file +): NostrEventEntity, ProfilePublicKeyEntity, TimestampedEntity, LocalStoreEntity, UserViewableEntity, BroadcastableEntity, SoftDeletableEntity { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as Repost + + if (id != other.id) return false + if (repostedPostId != other.repostedPostId) return false + if (repostedPostAuthorPublicKey != other.repostedPostAuthorPublicKey) return false + if (!repostedPostTags.contentDeepEquals(other.repostedPostTags)) return false + if (content != other.content) return false + if (nostrEventId != other.nostrEventId) return false + if (profilePublicKey != other.profilePublicKey) 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 + if (broadcastedAt != other.broadcastedAt) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + repostedPostId.hashCode() + result = 31 * result + repostedPostAuthorPublicKey.hashCode() + result = 31 * result + repostedPostTags.contentDeepHashCode() + result = 31 * result + content.hashCode() + result = 31 * result + nostrEventId.hashCode() + result = 31 * result + profilePublicKey.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) + result = 31 * result + (broadcastedAt?.hashCode() ?: 0) + return result + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/intermdiate/LocalNostrEvent.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/intermdiate/LocalNostrEvent.kt new file mode 100644 index 00000000..5b4085d6 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/model/intermdiate/LocalNostrEvent.kt @@ -0,0 +1,17 @@ +package ac.aux.compose.database.model.intermdiate + +import ac.aux.compose.database.model.NostrEvent +import ac.aux.compose.database.model.Post +import ac.aux.compose.database.model.Profile +import androidx.room.Embedded +import androidx.room.Relation + +data class LocalNostrEvent( + @Embedded val nostrEvent: NostrEvent, + + @Relation( + parentColumn = "pubKey", + entityColumn = "publicKey" + ) + val profile: Profile +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt index 5fb8ad74..e78c55bf 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/database/repository/DatabaseNostrRepository.kt @@ -8,25 +8,26 @@ import ac.aux.compose.database.model.Profile import ac.aux.compose.database.model.SynchronizeNostrEventRequest import ac.aux.compose.database.model.UnsignedNostrEvent import ac.aux.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest +import ac.aux.compose.database.model.intermdiate.LocalNostrEvent import ac.aux.compose.database.model.intermdiate.LocalProfile import ac.aux.compose.nostr.Relays import ac.aux.compose.repository.NostrRepository import co.touchlab.kermit.Logger import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent.Companion.ALT import com.vitorpamplona.quartz.nip02FollowList.RelaySet import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.tags.RelayTag +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -296,8 +297,14 @@ class DatabaseNostrRepository( ) } - override suspend fun loadNostrFeed(): List { - return database.nostrEventDao().getAllNostrEvents() + override suspend fun observeNostrFeed(): Flow> { + return database.nostrEventDao().observeNostrEvents( + kinds = arrayOf( + TextNoteEvent.KIND, + RepostEvent.KIND, + LnZapEvent.KIND + ) + ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt index b631c3c5..8945696b 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt @@ -7,6 +7,7 @@ import ac.aux.compose.database.model.Profile import ac.aux.compose.database.model.SynchronizeNostrEventRequest import ac.aux.compose.database.model.UnsignedNostrEvent import ac.aux.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest +import ac.aux.compose.database.model.intermdiate.LocalNostrEvent import ac.aux.compose.database.model.intermdiate.LocalProfile import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.coroutines.flow.Flow @@ -64,7 +65,7 @@ interface NostrRepository { synchronizeNostrEventRequests: List, ) - suspend fun loadNostrFeed(): List + suspend fun observeNostrFeed(): Flow> companion object { val NO_OP_NOSTR_REPOSITORY = object : NostrRepository { @@ -142,7 +143,7 @@ interface NostrRepository { TODO("Not yet implemented") } - override suspend fun loadNostrFeed(): List { + override suspend fun observeNostrFeed(): Flow> { TODO("Not yet implemented") } } 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 b3a7dffd..feda0569 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 @@ -31,7 +31,6 @@ import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable @@ -42,7 +41,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.quartz.nip01Core.core.Event @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -187,14 +185,12 @@ fun FeedScreen( } items( - items = feedListUIState.nostrEvents, - key = { e -> e.id } - ) { nostrEvent -> + items = feedListUIState.localNostrEvents, + key = { localNostrEvent -> localNostrEvent.nostrEvent.id } + ) { localNostrEvent -> FeedListItem( - nostrEvent = nostrEvent, - onNavigateToEvent = { - onNavigateToEvent.invoke(nostrEvent) - } + localNostrEvent = localNostrEvent, + onNavigateToEvent = onNavigateToEvent ) } } 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 2391beac..751d0660 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 @@ -1,6 +1,8 @@ package ac.aux.compose.ui.composable.widgets.feed import ac.aux.compose.database.model.NostrEvent +import ac.aux.compose.database.model.Profile +import ac.aux.compose.database.model.intermdiate.LocalNostrEvent import ac.aux.compose.extensions.toFormattedTimeAndDateString import ac.aux.compose.ui.theme.AuxTheme import androidx.compose.foundation.layout.Arrangement @@ -13,7 +15,9 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Person import androidx.compose.material3.AssistChip +import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SuggestionChip import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -24,78 +28,130 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @Composable -fun NostrEvent.ListView() { - when (kind) { - // TODO: Support other kinds... - else -> { +fun LocalNostrEvent.ListView( + onNavigateToEvent: (NostrEvent) -> Unit +) { + when (nostrEvent.kind) { + RepostEvent.KIND -> { + val repost = nostrEvent.toRepost() + // TODO: Make use of the indexed Repost field to render... + Column( - modifier = Modifier.padding(10.dp).fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(10.dp), - horizontalAlignment = Alignment.CenterHorizontally + modifier = Modifier.fillMaxWidth().padding(10.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) ) { Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.Start), - verticalAlignment = Alignment.CenterVertically + modifier = Modifier.fillMaxWidth() ) { - Icon( - Icons.Default.Person, - contentDescription = "Profile picture" + Text( + modifier = Modifier.weight(1f), + text = profile.displayName ?: profile.userName ?: profile.publicKey, + style = MaterialTheme.typography.labelSmall ) Text( - text = pubKey - ) - } - Text( - text = content, - ) - - LazyRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy( - 10.dp, - alignment = Alignment.Start - ) - ) { - item { - AssistChip( - label = { - Text( - text = kind.toString() // TODO: Kind by name... - ) - }, - onClick = { - - } - ) - - } - items( - items = tags, - ) { tag -> - SuggestionChip( - label = { - Text( - text = tag.joinToString(":") - ) - }, - onClick = { - - } - ) - } + text = "reposted", + style = MaterialTheme.typography.labelSmall) } - Text( - text = createdAt.toFormattedTimeAndDateString() + TextNoteFeedItem( + this@ListView, + onNavigateToEvent = onNavigateToEvent ) } } + // TODO: Support other kinds... + else -> { + TextNoteFeedItem( + this, + onNavigateToEvent = onNavigateToEvent + ) + } } } +@Composable +private fun TextNoteFeedItem( + localNostrEvent: LocalNostrEvent, + onNavigateToEvent: (NostrEvent) -> Unit +) { + ElevatedCard( + modifier = Modifier.padding( + horizontal = 10.dp + ), + onClick = { + onNavigateToEvent.invoke(localNostrEvent.nostrEvent) + } + ) { + Column( + modifier = Modifier.padding(10.dp).fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(10.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.Person, + contentDescription = "Profile picture", + ) + Text( + modifier = Modifier.weight(1f), + text = localNostrEvent.profile.displayName ?: localNostrEvent.profile.userName ?: localNostrEvent.profile.publicKey, + style = MaterialTheme.typography.bodyMedium + ) + } + Text( + text = localNostrEvent.nostrEvent.content, + ) + + LazyRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy( + 10.dp, + alignment = Alignment.Start + ) + ) { + item { + AssistChip( + label = { + Text( + text = localNostrEvent.nostrEvent.kind.toString() // TODO: Kind by name... + ) + }, + onClick = { + + } + ) + + } + items( + items = localNostrEvent.nostrEvent.tags, + ) { tag -> + SuggestionChip( + label = { + Text( + text = tag.joinToString(":") + ) + }, + onClick = { + + } + ) + } + } + + Text( + text = localNostrEvent.nostrEvent.createdAt.toFormattedTimeAndDateString() + ) + } + } +} @Preview @Composable private fun EventListViewPreview() { @@ -103,14 +159,23 @@ private fun EventListViewPreview() { Surface( modifier = Modifier.padding(20.dp) ) { - NostrEvent( - id = "hex", - content = "Something something seomthing", - pubKey = "hexpubkeypubkeypubkeypubkey", - kind = 0, - sig = "hexsig", - tags = emptyArray() - ).ListView() + LocalNostrEvent( + nostrEvent = NostrEvent( + id = "hex", + content = "Something something seomthing", + pubKey = "hexpubkeypubkeypubkeypubkey", + kind = 0, + sig = "hexsig", + tags = emptyArray() + ), + profile = Profile( + publicKey = "", + nostrEventId = "referenced", + displayName = "Frank Talk" + ) + ).ListView( + onNavigateToEvent = {} + ) } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/FeedListItem.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/FeedListItem.kt index f2fa11bc..416d0fbf 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/FeedListItem.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/FeedListItem.kt @@ -1,26 +1,19 @@ package ac.aux.compose.ui.composable.widgets.feed import ac.aux.compose.database.model.NostrEvent +import ac.aux.compose.database.model.intermdiate.LocalNostrEvent import androidx.compose.foundation.layout.padding import androidx.compose.material3.ElevatedCard import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.quartz.nip01Core.core.Event @Composable fun FeedListItem( - nostrEvent: NostrEvent, + localNostrEvent: LocalNostrEvent, onNavigateToEvent: (NostrEvent) -> Unit ) { - ElevatedCard( - modifier = Modifier.padding( - horizontal = 10.dp - ), - onClick = { - onNavigateToEvent.invoke(nostrEvent) - } - ) { - nostrEvent.ListView() - } + localNostrEvent.ListView( + onNavigateToEvent = onNavigateToEvent + ) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/FeedListViewModel.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/FeedListViewModel.kt index 108811bb..33456590 100755 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/FeedListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/FeedListViewModel.kt @@ -38,6 +38,30 @@ class FeedListViewModel( fun loadNostrFeed() { viewModelScope.launch(Dispatchers.IO) { + // Sync Feed +// nostrRepository.queueSynchronizeNostrEvent( +// Relays.eventPublishRelaySet.map { normalizedRelay -> +// SynchronizeNostrEventRequest( +// synchronizationFilters = arrayOf( +// SynchronizationFilter( +// kinds = arrayOf( +// TextNoteEvent.KIND, +// RepostEvent.KIND, +// ReactionEvent.KIND, +// LnZapEvent.KIND, +// ), +// tags = mapOf( +// Pair("p", listOf(SeedManager.activePublicKey().toHexKey())) +// ), +// limit = 50 +// ) +// ), +// relayURL = normalizedRelay.url +// ) +// } +// ) + + // Sync Notifications nostrRepository.queueSynchronizeNostrEvent( Relays.eventPublishRelaySet.map { normalizedRelay -> SynchronizeNostrEventRequest( @@ -67,11 +91,11 @@ class FeedListViewModel( if (localProfile?.profile == null) { feedListUIState = FeedListUIState.Error } else { - val nostrFeed = nostrRepository.loadNostrFeed() - - feedListUIState = FeedListUIState.Loaded( - nostrEvents = nostrFeed - ) + nostrRepository.observeNostrFeed().collect { localNostrEvents -> + feedListUIState = FeedListUIState.Loaded( + localNostrEvents = localNostrEvents + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/state/FeedListUIState.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/state/FeedListUIState.kt index 2d687cd9..d66a9caf 100755 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/state/FeedListUIState.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/state/FeedListUIState.kt @@ -1,10 +1,11 @@ package ac.aux.compose.ui.view.state import ac.aux.compose.database.model.NostrEvent +import ac.aux.compose.database.model.intermdiate.LocalNostrEvent sealed interface FeedListUIState { data class Loaded( - val nostrEvents: List + val localNostrEvents: List ): FeedListUIState data object Error: FeedListUIState