From 1cc2c6a98204c07f5edc96e91e6349b4194212cb Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Mon, 30 Mar 2026 03:03:35 +0200 Subject: [PATCH] Show feed of local nostr events. --- .../aux/compose/database/dao/NostrEventDao.kt | 2 +- .../repository/DatabaseNostrRepository.kt | 4 +++ .../aux/compose/repository/NostrRepository.kt | 6 ++++ .../aux/compose/ui/composable/FeedScreen.kt | 27 ++++++++------ .../ui/composable/navigation/AuxNavHost.kt | 8 ++--- .../ui/composable/widgets/feed/Dummy.kt | 29 --------------- .../composable/widgets/feed/EventListView.kt | 36 ++++++++++++++++--- .../composable/widgets/feed/FeedListItem.kt | 9 ++--- .../ui/view/model/FeedListViewModel.kt | 21 +++++++---- .../compose/ui/view/state/FeedListUIState.kt | 6 ++-- 10 files changed, 83 insertions(+), 65 deletions(-) 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 504b174e..8172ccc7 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 @@ -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 @Query("SELECT * FROM NostrEvent WHERE id = :id") 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 3843f2d0..a300f83a 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 @@ -204,4 +204,8 @@ class DatabaseNostrRepository( ) } + override suspend fun loadNostrFeed(): List { + return database.nostrEventDao().getAllNostrEvents() + } + } \ 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 1540e2df..b631c3c5 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/repository/NostrRepository.kt @@ -64,6 +64,8 @@ interface NostrRepository { synchronizeNostrEventRequests: List, ) + suspend fun loadNostrFeed(): List + companion object { val NO_OP_NOSTR_REPOSITORY = object : NostrRepository { override suspend fun observeProfile(publicKey: HexKey): Flow { @@ -139,6 +141,10 @@ interface NostrRepository { override suspend fun queueSynchronizeNostrEvent(synchronizeNostrEventRequests: List) { TODO("Not yet implemented") } + + override suspend fun loadNostrFeed(): List { + TODO("Not yet implemented") + } } } } \ No newline at end of file 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 66092f06..7b4259ec 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 @@ -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 ) } } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt index d1ebf684..43464efd 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/AuxNavHost.kt @@ -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 { 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 { diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/Dummy.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/Dummy.kt index b67d8e71..7d82ef27 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/Dummy.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/Dummy.kt @@ -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(), - ) -) 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 ca0844f3..f51377f6 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,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() } } } \ 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 71b3a7fa..41d4f840 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,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() } } \ 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 482f325e..650396fd 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 @@ -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 ) } } 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 feedde32..2d687cd9 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,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 + val nostrEvents: List ): FeedListUIState data object Error: FeedListUIState