Improve feed logic on profile detail

This commit is contained in:
Kgothatso Ngako committed 2026-04-22 03:25:19 +02:00
1 parent 2b693b832a
commit 810a690776
8 files changed
+235 -137

No files matched your search

@@ -22,6 +22,9 @@ interface ConnectionDao {
@Query("SELECT * FROM Connection WHERE destinationPublicKey = :destinationPublicKey") @Query("SELECT * FROM Connection WHERE destinationPublicKey = :destinationPublicKey")
fun getPublicKeyFollowers(destinationPublicKey: String): LocalFollowers? fun getPublicKeyFollowers(destinationPublicKey: String): LocalFollowers?
@Query("SELECT * FROM Connection WHERE destinationPublicKey = :destinationPublicKey")
fun observe(destinationPublicKey: String): LocalFollowers?
@Insert @Insert
suspend fun insertPlaceholderProfile(connection: Connection) suspend fun insertPlaceholderProfile(connection: Connection)
@@ -2,6 +2,8 @@ package ac.cord.auxiliary.compose.database.dao
import ac.cord.auxiliary.compose.database.model.Profile import ac.cord.auxiliary.compose.database.model.Profile
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfile import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfile
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowers
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
@@ -18,6 +20,12 @@ interface ProfileDao {
@Query("SELECT * FROM Profile WHERE publicKey = :publicKey") @Query("SELECT * FROM Profile WHERE publicKey = :publicKey")
fun getProfileByPublicKey(publicKey: String): Profile? fun getProfileByPublicKey(publicKey: String): Profile?
@Query("SELECT * FROM NostrEvent WHERE kind = 0 AND pubKey = :publicKey")
fun observeProfileWithFollowersByPublicKey(publicKey: String): LocalProfileWithFollowers?
@Query("SELECT * FROM NostrEvent WHERE kind = 0 AND pubKey = :publicKey")
fun observeProfileWithFollowingByPublicKey(publicKey: String): LocalProfileWithFollowing?
@Insert @Insert
suspend fun insertPlaceholderProfile(profile: Profile) suspend fun insertPlaceholderProfile(profile: Profile)
@@ -0,0 +1,29 @@
package ac.cord.auxiliary.compose.database.model.intermdiate
import ac.cord.auxiliary.compose.database.model.Connection
import ac.cord.auxiliary.compose.database.model.NostrEvent
import ac.cord.auxiliary.compose.database.model.Profile
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
data class LocalProfileWithFollowers(
@Embedded val nostrEvent: NostrEvent,
@Relation(
parentColumn = "pubKey",
entityColumn = "publicKey"
)
val profile: Profile,
@Relation(
parentColumn = "pubKey",
entityColumn = "publicKey",
associateBy = Junction(
value = Connection::class,
parentColumn = "destinationPublicKey",
entityColumn = "sourcePublicKey"
)
)
val followers: List<Profile>
)
@@ -0,0 +1,29 @@
package ac.cord.auxiliary.compose.database.model.intermdiate
import ac.cord.auxiliary.compose.database.model.Connection
import ac.cord.auxiliary.compose.database.model.NostrEvent
import ac.cord.auxiliary.compose.database.model.Profile
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
data class LocalProfileWithFollowing(
@Embedded val nostrEvent: NostrEvent,
@Relation(
parentColumn = "pubKey",
entityColumn = "publicKey"
)
val profile: Profile,
@Relation(
parentColumn = "pubKey",
entityColumn = "publicKey",
associateBy = Junction(
value = Connection::class,
parentColumn = "sourcePublicKey",
entityColumn = "destinationPublicKey"
)
)
val followers: List<Profile>
)
@@ -1,5 +1,7 @@
package ac.cord.auxiliary.compose.ui.composable package ac.cord.auxiliary.compose.ui.composable
import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.repository.NostrRepository import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.composable.widgets.LoadingDataIndicator import ac.cord.auxiliary.compose.ui.composable.widgets.LoadingDataIndicator
import ac.cord.auxiliary.compose.ui.composable.widgets.feed.FeedListItem import ac.cord.auxiliary.compose.ui.composable.widgets.feed.FeedListItem
@@ -43,6 +45,16 @@ 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.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
@@ -56,6 +68,23 @@ fun FeedScreen(
val feedListViewModel: FeedListViewModel = viewModel( val feedListViewModel: FeedListViewModel = viewModel(
factory = FeedListViewModel.factory( factory = FeedListViewModel.factory(
initialFeedListUIState = initialFeedListUIState, initialFeedListUIState = initialFeedListUIState,
synchronizationFilter = SynchronizationFilter(
kinds = arrayOf(
TextNoteEvent.KIND,
RepostEvent.KIND,
ReactionEvent.KIND,
LnZapEvent.KIND,
ContactListEvent.KIND,
AdvertisedRelayListEvent.KIND,
PrivateDmEvent.KIND,
BlossomServersEvent.KIND,
FileServersEvent.KIND
),
tags = mapOf(
Pair("p", listOf(SeedManager.activePublicKey().toHexKey()))
),
limit = 50
),
nostrRepository = nostrRepository nostrRepository = nostrRepository
), ),
) )
@@ -8,6 +8,7 @@ import ac.cord.auxiliary.compose.ui.composable.navigation.routes.NostrEventDetai
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.Route import ac.cord.auxiliary.compose.ui.composable.navigation.routes.Route
import ac.cord.auxiliary.compose.ui.composable.widgets.feed.ListView import ac.cord.auxiliary.compose.ui.composable.widgets.feed.ListView
import ac.cord.auxiliary.compose.ui.theme.AuxTheme import ac.cord.auxiliary.compose.ui.theme.AuxTheme
import ac.cord.auxiliary.compose.ui.view.model.FeedListViewModel
import ac.cord.auxiliary.compose.ui.view.model.MetadataEventDetailType import ac.cord.auxiliary.compose.ui.view.model.MetadataEventDetailType
import ac.cord.auxiliary.compose.ui.view.model.MetadataEventDetailViewModel import ac.cord.auxiliary.compose.ui.view.model.MetadataEventDetailViewModel
import ac.cord.auxiliary.compose.ui.view.model.SearchResultType import ac.cord.auxiliary.compose.ui.view.model.SearchResultType
@@ -216,81 +217,40 @@ fun MetadataEventDetail(
HorizontalPager( HorizontalPager(
state = metadataEventDetailViewModel.pagerState, state = metadataEventDetailViewModel.pagerState,
) { pageIndex -> ) { pageIndex ->
when (MetadataEventDetailType.entries[pageIndex]) {
MetadataEventDetailType.Posts, MetadataEventDetailType.Replies,
MetadataEventDetailType.Articles, MetadataEventDetailType.Zaps,
MetadataEventDetailType.Photos, MetadataEventDetailType.Videos,
MetadataEventDetailType.Bookmarks, MetadataEventDetailType.Reports, MetadataEventDetailType.Relays -> {
val feedListViewModel: FeedListViewModel = viewModel(
factory = FeedListViewModel.factory(
initialFeedListUIState = FeedListUIState.Loading,
synchronizationFilter = metadataEventDetailViewModel.getSynchronizationFilter(),
nostrRepository = nostrRepository
),
)
Column( feedListViewModel.RenderFeed(
modifier = Modifier.fillMaxWidth(), onNavigateToEvent = onNavigateToEvent
horizontalAlignment = Alignment.CenterHorizontally
) {
when (val feedListUIState = metadataEventDetailViewModel.feedListUIState) {
FeedListUIState.Error -> {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(
modifier = Modifier.height(50.dp)
)
Text(
text = "Something went wrong",
) )
} }
MetadataEventDetailType.Following -> {
Text("Implementing logic to see following")
} }
is FeedListUIState.Loaded -> { MetadataEventDetailType.Followers -> {
if (feedListUIState.localNostrEvents.isEmpty()) {
Column( Text("Implementing logic to see followers")
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(
modifier = Modifier.height(50.dp)
)
Text(
text = "No events were found",
)
}
} else {
LazyColumn(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(
items = feedListUIState.localNostrEvents,
key = { localNostrEvent -> localNostrEvent.nostrEvent.id}
) { localNostrEvent ->
localNostrEvent.ListView(
onNavigateToEvent = { nostrEventId ->
onNavigateToEvent.invoke(
NostrEventDetailRoute(
nostrEventId = nostrEventId
)
)
}
)
}
} }
} }
}
FeedListUIState.Loading -> {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(
modifier = Modifier.height(50.dp)
)
Text(
text = "Loading ${MetadataEventDetailType.entries[pageIndex].name.lowercase()}",
textAlign = TextAlign.Center
)
Spacer(
modifier = Modifier.height(50.dp)
)
LoadingIndicator() LaunchedEffect(true) {
} metadataEventDetailViewModel.updateMetadataEventDetailType(
} MetadataEventDetailType.Posts
) {
} }
} }
} }
@@ -5,11 +5,29 @@ import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
import ac.cord.auxiliary.compose.managers.SeedManager import ac.cord.auxiliary.compose.managers.SeedManager
import ac.cord.auxiliary.compose.nostr.Relays import ac.cord.auxiliary.compose.nostr.Relays
import ac.cord.auxiliary.compose.repository.NostrRepository import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.NostrEventDetailRoute
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.Route
import ac.cord.auxiliary.compose.ui.composable.widgets.feed.ListView
import ac.cord.auxiliary.compose.ui.view.state.FeedListUIState import ac.cord.auxiliary.compose.ui.view.state.FeedListUIState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@@ -33,6 +51,7 @@ import kotlinx.coroutines.launch
class FeedListViewModel( class FeedListViewModel(
initialFeedListUIState: FeedListUIState, initialFeedListUIState: FeedListUIState,
val synchronizationFilter: SynchronizationFilter,
val nostrRepository: NostrRepository val nostrRepository: NostrRepository
): ViewModel() { ): ViewModel() {
val logger = Logger.withTag(TAG) val logger = Logger.withTag(TAG)
@@ -58,7 +77,9 @@ class FeedListViewModel(
if (localProfile?.profile == null) { if (localProfile?.profile == null) {
feedListUIState = FeedListUIState.Error feedListUIState = FeedListUIState.Error
} else { } else {
nostrRepository.observeNostrFeed().collect { localNostrEvents -> nostrRepository.observeNostrFeed(
synchronizationFilter
).collect { localNostrEvents ->
feedListUIState = FeedListUIState.Loaded( feedListUIState = FeedListUIState.Loaded(
localNostrEvents = localNostrEvents localNostrEvents = localNostrEvents
) )
@@ -77,23 +98,7 @@ class FeedListViewModel(
SynchronizeNostrEventRequest( SynchronizeNostrEventRequest(
purpose = "feed", purpose = "feed",
synchronizationFilters = arrayOf( synchronizationFilters = arrayOf(
SynchronizationFilter( synchronizationFilter
kinds = arrayOf(
TextNoteEvent.KIND,
RepostEvent.KIND,
ReactionEvent.KIND,
LnZapEvent.KIND,
ContactListEvent.KIND,
AdvertisedRelayListEvent.KIND,
PrivateDmEvent.KIND,
BlossomServersEvent.KIND,
FileServersEvent.KIND
),
tags = mapOf(
Pair("p", listOf(SeedManager.activePublicKey().toHexKey()))
),
limit = 50
)
), ),
relayURL = normalizedRelay.url relayURL = normalizedRelay.url
) )
@@ -112,16 +117,101 @@ class FeedListViewModel(
} }
} }
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun RenderFeed(
onNavigateToEvent: (Route) -> Unit
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
when (val feedListUIState = feedListUIState) {
FeedListUIState.Error -> {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(
modifier = Modifier.height(50.dp)
)
Text(
text = "Something went wrong",
)
}
}
is FeedListUIState.Loaded -> {
if (feedListUIState.localNostrEvents.isEmpty()) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(
modifier = Modifier.height(50.dp)
)
Text(
text = "No events were found",
)
}
} else {
LazyColumn(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(
items = feedListUIState.localNostrEvents,
key = { localNostrEvent -> localNostrEvent.nostrEvent.id}
) { localNostrEvent ->
localNostrEvent.ListView(
onNavigateToEvent = { nostrEventId ->
onNavigateToEvent.invoke(
NostrEventDetailRoute(
nostrEventId = nostrEventId
)
)
}
)
}
}
}
}
FeedListUIState.Loading -> {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(
modifier = Modifier.height(50.dp)
)
Text(
text = "Loading",
textAlign = TextAlign.Center
)
Spacer(
modifier = Modifier.height(50.dp)
)
LoadingIndicator()
}
}
}
}
}
companion object { companion object {
const val TAG = "FeedListViewModel" const val TAG = "FeedListViewModel"
fun factory( fun factory(
initialFeedListUIState: FeedListUIState = FeedListUIState.Loading, initialFeedListUIState: FeedListUIState = FeedListUIState.Loading,
synchronizationFilter: SynchronizationFilter,
nostrRepository: NostrRepository, nostrRepository: NostrRepository,
): ViewModelProvider.Factory = viewModelFactory { ): ViewModelProvider.Factory = viewModelFactory {
initializer { initializer {
FeedListViewModel( FeedListViewModel(
initialFeedListUIState = initialFeedListUIState, initialFeedListUIState = initialFeedListUIState,
synchronizationFilter = synchronizationFilter,
nostrRepository = nostrRepository nostrRepository = nostrRepository
) )
} }
@@ -42,27 +42,16 @@ class MetadataEventDetailViewModel(
): ViewModel() { ): ViewModel() {
val logger = Logger.withTag(TAG) val logger = Logger.withTag(TAG)
var feedJob: Job? = null
var metadataEventDetailType: MetadataEventDetailType by mutableStateOf(initialMetadataEventDetailType) var metadataEventDetailType: MetadataEventDetailType by mutableStateOf(initialMetadataEventDetailType)
private set private set
var feedListUIState: FeedListUIState by mutableStateOf(FeedListUIState.Loading)
private set
fun updateMetadataEventDetailType (metadataEventDetailType: MetadataEventDetailType, navigateToPage: () -> Unit) { fun updateMetadataEventDetailType (metadataEventDetailType: MetadataEventDetailType, navigateToPage: () -> Unit) {
this.metadataEventDetailType = metadataEventDetailType this.metadataEventDetailType = metadataEventDetailType
initiateMetadataFeed(this.metadataEventDetailType)
navigateToPage.invoke() navigateToPage.invoke()
} }
fun initiateMetadataFeed(metadataEventDetailType: MetadataEventDetailType = this.metadataEventDetailType) { fun getSynchronizationFilter() = when (metadataEventDetailType) {
feedListUIState = FeedListUIState.Loading
logger.d("initiateMetadataFeed")
val synchronizationFilter = when (metadataEventDetailType) {
MetadataEventDetailType.Posts -> { MetadataEventDetailType.Posts -> {
SynchronizationFilter( SynchronizationFilter(
authors = arrayOf( authors = arrayOf(
@@ -193,47 +182,8 @@ class MetadataEventDetailViewModel(
} }
} }
syncFeed(synchronizationFilter)
loadFeed(synchronizationFilter)
}
private fun syncFeed(synchronizationFilter: SynchronizationFilter) {
logger.d("syncFeed")
viewModelScope.launch(Dispatchers.IO) {
// Sync Notifications... might want to also run this in the background
nostrRepository.queueSynchronizeNostrEvent(
Relays.eventFinderRelaySet.take(1).map { normalizedRelay -> // TODO: Sync from all relays instead of 1
SynchronizeNostrEventRequest(
purpose = "search",
synchronizationFilters = arrayOf(
synchronizationFilter
),
relayURL = normalizedRelay.url
)
}
)
}
}
private fun loadFeed(synchronizationFilter: SynchronizationFilter) {
logger.d("loadFeed")
feedJob?.cancel()
feedJob = viewModelScope.launch(Dispatchers.IO) {
// This is something not taking into consideration the results we are actually looking for...
nostrRepository.observeNostrFeed(synchronizationFilter).collect { localNostrEvents ->
feedListUIState = FeedListUIState.Loaded(
localNostrEvents = localNostrEvents
)
}
}
}
companion object { companion object {
const val TAG = "SearchViewModel" const val TAG = "MetadataEventDetailViewModel"
fun factory( fun factory(
initialMetadataEventDetailType: MetadataEventDetailType = MetadataEventDetailType.Posts, initialMetadataEventDetailType: MetadataEventDetailType = MetadataEventDetailType.Posts,