Show feed of local nostr events.

This commit is contained in:
Kgothatso Ngako
2026-03-30 03:03:35 +02:00
parent f37ec2c904
commit 1cc2c6a982
10 changed files with 83 additions and 65 deletions

View File

@@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface NostrEventDao {
@Query("SELECT * FROM NostrEvent")
@Query("SELECT * FROM NostrEvent ORDER BY createdAt DESC")
fun getAllNostrEvents(): List<NostrEvent>
@Query("SELECT * FROM NostrEvent WHERE id = :id")

View File

@@ -204,4 +204,8 @@ class DatabaseNostrRepository(
)
}
override suspend fun loadNostrFeed(): List<NostrEvent> {
return database.nostrEventDao().getAllNostrEvents()
}
}

View File

@@ -64,6 +64,8 @@ interface NostrRepository {
synchronizeNostrEventRequests: List<SynchronizeNostrEventRequest>,
)
suspend fun loadNostrFeed(): List<NostrEvent>
companion object {
val NO_OP_NOSTR_REPOSITORY = object : NostrRepository {
override suspend fun observeProfile(publicKey: HexKey): Flow<LocalProfile?> {
@@ -139,6 +141,10 @@ interface NostrRepository {
override suspend fun queueSynchronizeNostrEvent(synchronizeNostrEventRequests: List<SynchronizeNostrEventRequest>) {
TODO("Not yet implemented")
}
override suspend fun loadNostrFeed(): List<NostrEvent> {
TODO("Not yet implemented")
}
}
}
}

View File

@@ -1,8 +1,9 @@
package ac.aux.compose.ui.composable
import ac.aux.compose.database.model.NostrEvent
import ac.aux.compose.repository.NostrRepository
import ac.aux.compose.ui.composable.widgets.LoadingDataIndicator
import ac.aux.compose.ui.composable.widgets.feed.FeedListItem
import ac.aux.compose.ui.composable.widgets.feed.ListView
import ac.aux.compose.ui.theme.AuxTheme
import ac.aux.compose.ui.view.model.FeedListViewModel
import ac.aux.compose.ui.view.state.FeedListUIState
@@ -14,8 +15,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Adb
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -30,6 +29,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -37,19 +37,19 @@ 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
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import kotlin.time.Clock
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FeedScreen(
initialFeedListUIState: FeedListUIState,
onNavigateToEvent: (Event) -> Unit,
onNavigateToEvent: (NostrEvent) -> Unit,
onNavigateToWriteNewNote: () -> Unit,
nostrRepository: NostrRepository,
) {
val feedListViewModel: FeedListViewModel = viewModel(
factory = FeedListViewModel.factory(
initialFeedListUIState = initialFeedListUIState,
nostrRepository = nostrRepository
),
)
@@ -107,6 +107,10 @@ fun FeedScreen(
when(val feedListUIState = feedListViewModel.feedListUIState) {
is FeedListUIState.Loading -> {
LoadingDataIndicator()
LaunchedEffect(true) {
feedListViewModel.loadNostrFeed()
}
}
FeedListUIState.Error -> {
Text("Something went wrong")
@@ -118,13 +122,13 @@ fun FeedScreen(
horizontalAlignment = Alignment.CenterHorizontally
) {
items(
items = feedListUIState.events,
items = feedListUIState.nostrEvents,
key = { e -> e.id }
) { event ->
) { nostrEvent ->
FeedListItem(
event = event,
nostrEvent = nostrEvent,
onNavigateToEvent = {
onNavigateToEvent.invoke(event)
onNavigateToEvent.invoke(nostrEvent)
}
)
}
@@ -145,7 +149,8 @@ private fun FeedScreenPreview() {
FeedScreen(
initialFeedListUIState = FeedListUIState.Loading,
onNavigateToEvent = {},
onNavigateToWriteNewNote = {}
onNavigateToWriteNewNote = {},
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY
)
}
}

View File

@@ -37,7 +37,6 @@ import ac.aux.compose.ui.composable.navigation.routes.UnqueuedProfileSynchroniza
import ac.aux.compose.ui.composable.navigation.routes.UnsignedProfileRoute
import ac.aux.compose.ui.composable.navigation.routes.UnsyncedProfileRoute
import ac.aux.compose.ui.composable.navigation.routes.WriteNewNoteRoute
import ac.aux.compose.ui.composable.widgets.feed.DUMMY_EVENTS
import ac.aux.compose.ui.view.model.NavigationViewModel
import ac.aux.compose.ui.view.state.FeedListUIState
import ac.aux.compose.ui.view.state.NavigationUIState
@@ -295,9 +294,7 @@ fun AuxNavHost(
}
composable<FeedRoute> {
FeedScreen(
initialFeedListUIState = FeedListUIState.Loaded(
events = DUMMY_EVENTS,
),
initialFeedListUIState = FeedListUIState.Loading,
onNavigateToEvent = { event ->
navController.navigate(
route = ImplementationPendingRoute(
@@ -309,7 +306,8 @@ fun AuxNavHost(
navController.navigate(
route = WriteNewNoteRoute()
)
}
},
nostrRepository = nostrRepository
)
}
composable<BlankRoute> {

View File

@@ -4,32 +4,3 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import kotlin.time.Clock
val DUMMY_EVENTS = listOf(
Event(
id = KeyPair().pubKey.toString(),
pubKey = KeyPair().pubKey.toString(),
createdAt = Clock.System.now().toEpochMilliseconds(),
kind = 1,
tags = emptyArray(),
content = "This is just a note",
sig = KeyPair().pubKey.toString(),
),
Event(
id = KeyPair().pubKey.toString(),
pubKey = KeyPair().pubKey.toString(),
createdAt = Clock.System.now().toEpochMilliseconds(),
kind = 1,
tags = emptyArray(),
content = "This is another note",
sig = KeyPair().pubKey.toString(),
),
Event(
id = KeyPair().pubKey.toString(),
pubKey = KeyPair().pubKey.toString(),
createdAt = Clock.System.now().toEpochMilliseconds(),
kind = 1,
tags = emptyArray(),
content = "This is a better note",
sig = KeyPair().pubKey.toString(),
)
)

View File

@@ -1,5 +1,6 @@
package ac.aux.compose.ui.composable.widgets.feed
import ac.aux.compose.database.model.NostrEvent
import ac.aux.compose.ui.theme.AuxTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -8,19 +9,23 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
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.Icon
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.BiasAbsoluteAlignment
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
@Composable
fun Event.ListView() {
fun NostrEvent.ListView() {
when (kind) {
// TODO: Support other kinds...
else -> {
@@ -29,8 +34,18 @@ fun Event.ListView() {
verticalArrangement = Arrangement.spacedBy(10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.Start),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.Person,
contentDescription = "Profile picture"
)
Text(
text = pubKey
)
}
Text(
text = content,
@@ -62,7 +77,7 @@ fun Event.ListView() {
SuggestionChip(
label = {
Text(
text = tag.toString()
text = tag.joinToString(":")
)
},
onClick = {
@@ -71,6 +86,10 @@ fun Event.ListView() {
)
}
}
Text(
text = createdAt.toString()
)
}
}
}
@@ -83,7 +102,14 @@ private fun EventListViewPreview() {
Surface(
modifier = Modifier.padding(20.dp)
) {
DUMMY_EVENTS.first().ListView()
NostrEvent(
id = "hex",
content = "Something something seomthing",
pubKey = "hexpubkeypubkeypubkeypubkey",
kind = 0,
sig = "hexsig",
tags = emptyArray()
).ListView()
}
}
}

View File

@@ -1,19 +1,20 @@
package ac.aux.compose.ui.composable.widgets.feed
import ac.aux.compose.database.model.NostrEvent
import androidx.compose.material3.ElevatedCard
import androidx.compose.runtime.Composable
import com.vitorpamplona.quartz.nip01Core.core.Event
@Composable
fun FeedListItem(
event: Event,
onNavigateToEvent: (Event) -> Unit
nostrEvent: NostrEvent,
onNavigateToEvent: (NostrEvent) -> Unit
) {
ElevatedCard(
onClick = {
onNavigateToEvent.invoke(event)
onNavigateToEvent.invoke(nostrEvent)
}
) {
event.ListView()
nostrEvent.ListView()
}
}

View File

@@ -1,5 +1,6 @@
package ac.aux.compose.ui.view.model
import ac.aux.compose.repository.NostrRepository
import ac.aux.compose.ui.view.state.FeedListUIState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -9,35 +10,43 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
class FeedListViewModel(
initialFeedListUIState: FeedListUIState,
val nostrRepository: NostrRepository
): ViewModel() {
var feedListUIState: FeedListUIState by mutableStateOf(initialFeedListUIState)
private set
fun retryLoad() {
feedListUIState = FeedListUIState.Loading
loadRecipientListData()
loadNostrFeed()
}
fun loadRecipientListData() {
viewModelScope.launch {
// TODO: Load Dummy data...
fun loadNostrFeed() {
viewModelScope.launch(Dispatchers.IO) {
val nostrFeed = nostrRepository.loadNostrFeed()
feedListUIState = FeedListUIState.Loaded(
nostrEvents = nostrFeed
)
}
}
companion object {
const val TAG = "RecipientListViewModel"
const val TAG = "FeedListViewModel"
fun factory(
initialFeedListUIState: FeedListUIState = FeedListUIState.Loading,
nostrRepository: NostrRepository,
): ViewModelProvider.Factory = viewModelFactory {
initializer {
FeedListViewModel(
initialFeedListUIState = initialFeedListUIState,
nostrRepository = nostrRepository
)
}
}

View File

@@ -1,12 +1,10 @@
package ac.aux.compose.ui.view.state
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import kotlin.time.Clock
import ac.aux.compose.database.model.NostrEvent
sealed interface FeedListUIState {
data class Loaded(
val events: List<Event>
val nostrEvents: List<NostrEvent>
): FeedListUIState
data object Error: FeedListUIState