Handling reposts

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

View File

@@ -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')"
]
}
}

View File

@@ -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<String>
) {
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<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 ->
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...

View File

@@ -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<NostrEvent>
@Query("SELECT * FROM NostrEvent WHERE kind in (:kinds) ORDER BY savedAt DESC")
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")
fun getNostrEventById(id: String): Flow<NostrEvent?>

View File

@@ -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)
}

View File

@@ -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),
)
}
}

View File

@@ -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
): 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
}
}

View File

@@ -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
)

View File

@@ -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<NostrEvent> {
return database.nostrEventDao().getAllNostrEvents()
override suspend fun observeNostrFeed(): Flow<List<LocalNostrEvent>> {
return database.nostrEventDao().observeNostrEvents(
kinds = arrayOf(
TextNoteEvent.KIND,
RepostEvent.KIND,
LnZapEvent.KIND
)
)
}
}

View File

@@ -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<SynchronizeNostrEventRequest>,
)
suspend fun loadNostrFeed(): List<NostrEvent>
suspend fun observeNostrFeed(): Flow<List<LocalNostrEvent>>
companion object {
val NO_OP_NOSTR_REPOSITORY = object : NostrRepository {
@@ -142,7 +143,7 @@ interface NostrRepository {
TODO("Not yet implemented")
}
override suspend fun loadNostrFeed(): List<NostrEvent> {
override suspend fun observeNostrFeed(): Flow<List<LocalNostrEvent>> {
TODO("Not yet implemented")
}
}

View File

@@ -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
)
}
}

View File

@@ -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 = {}
)
}
}
}

View File

@@ -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
)
}

View File

@@ -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
)
}
}
}
}

View File

@@ -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<NostrEvent>
val localNostrEvents: List<LocalNostrEvent>
): FeedListUIState
data object Error: FeedListUIState