Handling reposts

This commit is contained in:
Kgothatso Ngako committed 2026-03-31 16:13:44 +02:00
1 parent 3cefeae5c1
commit c1d7572d44
14 files changed
+383 -161

No files matched your search

@@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "0e3d4940d3b045a7e68c2f0b98642ab0", "identityHash": "6c8c4b9a4b44bed3c7fde29fcf54d7df",
"entities": [ "entities": [
{ {
"tableName": "BroadcastNostrEventReceipt", "tableName": "BroadcastNostrEventReceipt",
@@ -735,7 +735,7 @@
}, },
{ {
"tableName": "Repost", "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": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@@ -749,6 +749,30 @@
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "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", "fieldPath": "content",
"columnName": "content", "columnName": "content",
@@ -848,6 +872,17 @@
"publicKey" "publicKey"
] ]
}, },
{
"table": "Profile",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"repostedPostAuthorPublicKey"
],
"referencedColumns": [
"publicKey"
]
},
{ {
"table": "NostrEvent", "table": "NostrEvent",
"onDelete": "CASCADE", "onDelete": "CASCADE",
@@ -1307,7 +1342,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, '0e3d4940d3b045a7e68c2f0b98642ab0')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6c8c4b9a4b44bed3c7fde29fcf54d7df')"
] ]
} }
} }
@@ -44,7 +44,7 @@ abstract class NostrDao(
) )
val placeHolderProfileNostrEvent = NostrEvent( val placeHolderProfileNostrEvent = NostrEvent(
id = placeHolderProfile.publicKey, id = placeHolderProfile.publicKey,
kind = 30024, kind = DraftWrapEvent.KIND,
pubKey = placeHolderProfile.publicKey, pubKey = placeHolderProfile.publicKey,
content = placeHolderProfile.publicKey, content = placeHolderProfile.publicKey,
createdAt = Clock.System.now(), createdAt = Clock.System.now(),
@@ -81,44 +81,6 @@ abstract class NostrDao(
synchronizationRelayURLs: List<String> synchronizationRelayURLs: List<String>
) { ) {
logger.i("Store Nostr Event: $nostrEvent") 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) database.nostrEventDao().upsert(nostrEvent)
// Index nostrEvent // Index nostrEvent
@@ -132,6 +94,34 @@ abstract class NostrDao(
nostrEvent: NostrEvent, nostrEvent: NostrEvent,
synchronizationRelayURLs: List<String> synchronizationRelayURLs: List<String>
) { ) {
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 -> 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
@@ -149,8 +139,8 @@ abstract class NostrDao(
) )
// Request a sync for the post being replied to // Request a sync for the post being replied to
synchronizationRelayURLs.forEach { synchronizationRelayURL -> database.synchronizeNostrEventRequestDao().insert(
database.synchronizeNostrEventRequestDao().upsert( synchronizationRelayURLs.map { synchronizationRelayURL ->
SynchronizeNostrEventRequest( SynchronizeNostrEventRequest(
synchronizationFilters = arrayOf( synchronizationFilters = arrayOf(
SynchronizationFilter( SynchronizationFilter(
@@ -159,8 +149,8 @@ abstract class NostrDao(
), ),
relayURL = synchronizationRelayURL relayURL = synchronizationRelayURL
) )
) }
} )
} }
} }
@@ -178,8 +168,8 @@ abstract class NostrDao(
) )
) )
// Request a sync for the post being reposted // Request a sync for the post being reposted
synchronizationRelayURLs.forEach { synchronizationRelayURL -> database.synchronizeNostrEventRequestDao().insert(
database.synchronizeNostrEventRequestDao().upsert( synchronizationRelayURLs.map { synchronizationRelayURL ->
SynchronizeNostrEventRequest( SynchronizeNostrEventRequest(
synchronizationFilters = arrayOf( synchronizationFilters = arrayOf(
SynchronizationFilter( SynchronizationFilter(
@@ -188,11 +178,11 @@ abstract class NostrDao(
), ),
relayURL = synchronizationRelayURL relayURL = synchronizationRelayURL
) )
) }
} )
} }
} }
database.postDao().insert(post) database.postDao().upsert(post)
} }
nostrEvent.toProfile()?.let { profile -> nostrEvent.toProfile()?.let { profile ->
@@ -219,8 +209,8 @@ abstract class NostrDao(
) )
// Request a sync for the post being replied to // Request a sync for the post being replied to
synchronizationRelayURLs.forEach { synchronizationRelayURL -> database.synchronizeNostrEventRequestDao().insert(
database.synchronizeNostrEventRequestDao().upsert( synchronizationRelayURLs.map { synchronizationRelayURL ->
SynchronizeNostrEventRequest( SynchronizeNostrEventRequest(
synchronizationFilters = arrayOf( synchronizationFilters = arrayOf(
SynchronizationFilter( SynchronizationFilter(
@@ -229,8 +219,35 @@ abstract class NostrDao(
), ),
relayURL = synchronizationRelayURL 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 // Request a sync for the post being replied to
synchronizationRelayURLs.forEach { synchronizationRelayURL -> database.synchronizeNostrEventRequestDao().insert(
database.synchronizeNostrEventRequestDao().upsert( synchronizationRelayURLs.map { synchronizationRelayURL ->
SynchronizeNostrEventRequest( SynchronizeNostrEventRequest(
synchronizationFilters = arrayOf( synchronizationFilters = arrayOf(
SynchronizationFilter( SynchronizationFilter(
@@ -260,8 +277,8 @@ abstract class NostrDao(
), ),
relayURL = synchronizationRelayURL relayURL = synchronizationRelayURL
) )
) }
} )
} }
// TODO: Placeholder for all the other things... // TODO: Placeholder for all the other things...
@@ -1,17 +1,22 @@
package ac.aux.compose.database.dao package ac.aux.compose.database.dao
import ac.aux.compose.database.model.NostrEvent import ac.aux.compose.database.model.NostrEvent
import ac.aux.compose.database.model.intermdiate.LocalNostrEvent
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy.Companion.IGNORE import androidx.room.OnConflictStrategy.Companion.IGNORE
import androidx.room.Query import androidx.room.Query
import androidx.room.Upsert import androidx.room.Upsert
import com.vitorpamplona.quartz.nip01Core.core.Kind
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface NostrEventDao { interface NostrEventDao {
@Query("SELECT * FROM NostrEvent ORDER BY createdAt DESC") @Query("SELECT * FROM NostrEvent WHERE kind in (:kinds) ORDER BY savedAt DESC")
fun getAllNostrEvents(): List<NostrEvent> fun observeNostrEvents(kinds: Array<Kind>): Flow<List<LocalNostrEvent>>
@Query("SELECT * FROM NostrEvent WHERE kind in (:kinds) ORDER BY savedAt DESC")
fun getAllNostrEvents(kinds: Array<Kind>): List<NostrEvent>
@Query("SELECT * FROM NostrEvent WHERE id = :id") @Query("SELECT * FROM NostrEvent WHERE id = :id")
fun getNostrEventById(id: String): Flow<NostrEvent?> fun getNostrEventById(id: String): Flow<NostrEvent?>
@@ -8,6 +8,7 @@ import androidx.room.Embedded
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.Relation import androidx.room.Relation
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
@@ -20,4 +21,7 @@ interface PostDao {
@Insert @Insert
suspend fun insert(post: Post) suspend fun insert(post: Post)
@Upsert
suspend fun upsert(post: Post)
} }
@@ -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.TimestampedEntity
import ac.aux.compose.database.model.traits.UnsignedNostrEventEntity import ac.aux.compose.database.model.traits.UnsignedNostrEventEntity
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Ignore import androidx.room.Ignore
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
@@ -197,18 +196,23 @@ data class NostrEvent(
sig = sig sig = sig
) )
return repostEvent.boostedEventId()?.let { repostedPostId -> return repostEvent.containedPost()?.let { containedPost ->
Repost( Repost(
id = repostEvent.id, id = repostEvent.id,
repostedPostId = repostedPostId, repostedPostId = containedPost.id,
repostedPostTags = containedPost.tags,
repostedPostContent = containedPost.content,
repostedPostCreatedAt = Instant.fromEpochSeconds(containedPost.createdAt),
content = repostEvent.content, content = repostEvent.content,
nostrEventId = id, nostrEventId = id,
profilePublicKey = repostEvent.pubKey, profilePublicKey = repostEvent.pubKey,
repostedPostAuthorPublicKey = containedPost.pubKey,
createdAt = Instant.fromEpochSeconds(repostEvent.createdAt), createdAt = Instant.fromEpochSeconds(repostEvent.createdAt),
updatedAt = Instant.fromEpochSeconds(repostEvent.createdAt), updatedAt = Instant.fromEpochSeconds(repostEvent.createdAt),
) )
} }
} }
@@ -12,6 +12,7 @@ import androidx.room.ForeignKey
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Instant import kotlin.time.Instant
@@ -23,6 +24,12 @@ import kotlin.time.Instant
childColumns = ["profilePublicKey"], childColumns = ["profilePublicKey"],
onDelete = ForeignKey.CASCADE, 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... // This might cause problems... but we gonna make the feed so cool if it works...
ForeignKey( ForeignKey(
entity = NostrEvent::class, entity = NostrEvent::class,
@@ -47,7 +54,13 @@ import kotlin.time.Instant
data class Repost( data class Repost(
@PrimaryKey @PrimaryKey
val id: HexKey, val id: HexKey,
val repostedPostId: HexKey, val repostedPostId: HexKey,
val repostedPostTags: TagArray,
val repostedPostCreatedAt: Instant,
val repostedPostContent: String,
val repostedPostAuthorPublicKey: HexKey,
val content: String, val content: String,
override val nostrEventId: HexKey, // Might be a replaceable nostr event? override val nostrEventId: HexKey, // Might be a replaceable nostr event?
@@ -59,4 +72,44 @@ data class Repost(
override val viewedAt: Instant? = null, override val viewedAt: Instant? = null,
override val deletedAt: Instant? = null, override val deletedAt: Instant? = null,
override val broadcastedAt: Instant? = null, override val broadcastedAt: Instant? = null,
): NostrEventEntity, ProfilePublicKeyEntity, TimestampedEntity, LocalStoreEntity, UserViewableEntity, BroadcastableEntity, SoftDeletableEntity ): 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
}
}
@@ -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
)
@@ -8,25 +8,26 @@ import ac.aux.compose.database.model.Profile
import ac.aux.compose.database.model.SynchronizeNostrEventRequest import ac.aux.compose.database.model.SynchronizeNostrEventRequest
import ac.aux.compose.database.model.UnsignedNostrEvent import ac.aux.compose.database.model.UnsignedNostrEvent
import ac.aux.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest 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.database.model.intermdiate.LocalProfile
import ac.aux.compose.nostr.Relays import ac.aux.compose.nostr.Relays
import ac.aux.compose.repository.NostrRepository import ac.aux.compose.repository.NostrRepository
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.vitorpamplona.quartz.nip01Core.core.HexKey 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.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent 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.RelaySet
import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip51Lists.tags.RelayTag import com.vitorpamplona.quartz.nip51Lists.tags.RelayTag
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -296,8 +297,14 @@ class DatabaseNostrRepository(
) )
} }
override suspend fun loadNostrFeed(): List<NostrEvent> { override suspend fun observeNostrFeed(): Flow<List<LocalNostrEvent>> {
return database.nostrEventDao().getAllNostrEvents() return database.nostrEventDao().observeNostrEvents(
kinds = arrayOf(
TextNoteEvent.KIND,
RepostEvent.KIND,
LnZapEvent.KIND
)
)
} }
} }
@@ -7,6 +7,7 @@ import ac.aux.compose.database.model.Profile
import ac.aux.compose.database.model.SynchronizeNostrEventRequest import ac.aux.compose.database.model.SynchronizeNostrEventRequest
import ac.aux.compose.database.model.UnsignedNostrEvent import ac.aux.compose.database.model.UnsignedNostrEvent
import ac.aux.compose.database.model.intermdiate.LocalBroadcastNostrEventRequest 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.database.model.intermdiate.LocalProfile
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -64,7 +65,7 @@ interface NostrRepository {
synchronizeNostrEventRequests: List<SynchronizeNostrEventRequest>, synchronizeNostrEventRequests: List<SynchronizeNostrEventRequest>,
) )
suspend fun loadNostrFeed(): List<NostrEvent> suspend fun observeNostrFeed(): Flow<List<LocalNostrEvent>>
companion object { companion object {
val NO_OP_NOSTR_REPOSITORY = object : NostrRepository { val NO_OP_NOSTR_REPOSITORY = object : NostrRepository {
@@ -142,7 +143,7 @@ interface NostrRepository {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun loadNostrFeed(): List<NostrEvent> { override suspend fun observeNostrFeed(): Flow<List<LocalNostrEvent>> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }
@@ -31,7 +31,6 @@ import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.quartz.nip01Core.core.Event
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -187,14 +185,12 @@ fun FeedScreen(
} }
items( items(
items = feedListUIState.nostrEvents, items = feedListUIState.localNostrEvents,
key = { e -> e.id } key = { localNostrEvent -> localNostrEvent.nostrEvent.id }
) { nostrEvent -> ) { localNostrEvent ->
FeedListItem( FeedListItem(
nostrEvent = nostrEvent, localNostrEvent = localNostrEvent,
onNavigateToEvent = { onNavigateToEvent = onNavigateToEvent
onNavigateToEvent.invoke(nostrEvent)
}
) )
} }
} }
@@ -1,6 +1,8 @@
package ac.aux.compose.ui.composable.widgets.feed package ac.aux.compose.ui.composable.widgets.feed
import ac.aux.compose.database.model.NostrEvent 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.extensions.toFormattedTimeAndDateString
import ac.aux.compose.ui.theme.AuxTheme import ac.aux.compose.ui.theme.AuxTheme
import androidx.compose.foundation.layout.Arrangement 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.Icons
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChip
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
@Composable @Composable
fun NostrEvent.ListView() { fun LocalNostrEvent.ListView(
when (kind) { onNavigateToEvent: (NostrEvent) -> Unit
// TODO: Support other kinds... ) {
else -> { when (nostrEvent.kind) {
RepostEvent.KIND -> {
val repost = nostrEvent.toRepost()
// TODO: Make use of the indexed Repost field to render...
Column( Column(
modifier = Modifier.padding(10.dp).fillMaxWidth(), modifier = Modifier.fillMaxWidth().padding(10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp), horizontalAlignment = Alignment.CenterHorizontally,
horizontalAlignment = Alignment.CenterHorizontally verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.Start),
verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Text(
Icons.Default.Person, modifier = Modifier.weight(1f),
contentDescription = "Profile picture" text = profile.displayName ?: profile.userName ?: profile.publicKey,
style = MaterialTheme.typography.labelSmall
) )
Text( Text(
text = pubKey text = "reposted",
) style = MaterialTheme.typography.labelSmall)
}
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( TextNoteFeedItem(
text = createdAt.toFormattedTimeAndDateString() 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 @Preview
@Composable @Composable
private fun EventListViewPreview() { private fun EventListViewPreview() {
@@ -103,14 +159,23 @@ private fun EventListViewPreview() {
Surface( Surface(
modifier = Modifier.padding(20.dp) modifier = Modifier.padding(20.dp)
) { ) {
NostrEvent( LocalNostrEvent(
id = "hex", nostrEvent = NostrEvent(
content = "Something something seomthing", id = "hex",
pubKey = "hexpubkeypubkeypubkeypubkey", content = "Something something seomthing",
kind = 0, pubKey = "hexpubkeypubkeypubkeypubkey",
sig = "hexsig", kind = 0,
tags = emptyArray() sig = "hexsig",
).ListView() tags = emptyArray()
),
profile = Profile(
publicKey = "",
nostrEventId = "referenced",
displayName = "Frank Talk"
)
).ListView(
onNavigateToEvent = {}
)
} }
} }
} }
@@ -1,26 +1,19 @@
package ac.aux.compose.ui.composable.widgets.feed package ac.aux.compose.ui.composable.widgets.feed
import ac.aux.compose.database.model.NostrEvent import ac.aux.compose.database.model.NostrEvent
import ac.aux.compose.database.model.intermdiate.LocalNostrEvent
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.quartz.nip01Core.core.Event
@Composable @Composable
fun FeedListItem( fun FeedListItem(
nostrEvent: NostrEvent, localNostrEvent: LocalNostrEvent,
onNavigateToEvent: (NostrEvent) -> Unit onNavigateToEvent: (NostrEvent) -> Unit
) { ) {
ElevatedCard( localNostrEvent.ListView(
modifier = Modifier.padding( onNavigateToEvent = onNavigateToEvent
horizontal = 10.dp )
),
onClick = {
onNavigateToEvent.invoke(nostrEvent)
}
) {
nostrEvent.ListView()
}
} }
@@ -38,6 +38,30 @@ class FeedListViewModel(
fun loadNostrFeed() { fun loadNostrFeed() {
viewModelScope.launch(Dispatchers.IO) { 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( nostrRepository.queueSynchronizeNostrEvent(
Relays.eventPublishRelaySet.map { normalizedRelay -> Relays.eventPublishRelaySet.map { normalizedRelay ->
SynchronizeNostrEventRequest( SynchronizeNostrEventRequest(
@@ -67,11 +91,11 @@ class FeedListViewModel(
if (localProfile?.profile == null) { if (localProfile?.profile == null) {
feedListUIState = FeedListUIState.Error feedListUIState = FeedListUIState.Error
} else { } else {
val nostrFeed = nostrRepository.loadNostrFeed() nostrRepository.observeNostrFeed().collect { localNostrEvents ->
feedListUIState = FeedListUIState.Loaded(
feedListUIState = FeedListUIState.Loaded( localNostrEvents = localNostrEvents
nostrEvents = nostrFeed )
) }
} }
} }
} }
@@ -1,10 +1,11 @@
package ac.aux.compose.ui.view.state package ac.aux.compose.ui.view.state
import ac.aux.compose.database.model.NostrEvent import ac.aux.compose.database.model.NostrEvent
import ac.aux.compose.database.model.intermdiate.LocalNostrEvent
sealed interface FeedListUIState { sealed interface FeedListUIState {
data class Loaded( data class Loaded(
val nostrEvents: List<NostrEvent> val localNostrEvents: List<LocalNostrEvent>
): FeedListUIState ): FeedListUIState
data object Error: FeedListUIState data object Error: FeedListUIState