From 3e9566d0e9dad056f85929e465074c0fdb53fae6 Mon Sep 17 00:00:00 2001 From: Kgothatso Ngako Date: Mon, 23 Mar 2026 15:39:40 +0200 Subject: [PATCH] Show something on the feed --- .../aux/compose/ui/composable/FeedScreen.kt | 98 +++++++++++++++---- .../ui/composable/SocialPreconditionScreen.kt | 93 ++++++++++++++++-- .../ui/composable/navigation/AuxNavHost.kt | 41 +++++++- .../composable/navigation/routes/FeedRoute.kt | 6 ++ .../routes/SocialPreconditionRoute.kt | 6 ++ .../ui/composable/widgets/feed/Dummy.kt | 35 +++++++ .../composable/widgets/feed/EventListView.kt | 85 ++++++++++++++++ .../composable/widgets/feed/FeedListItem.kt | 19 ++++ .../composable/widgets/feed/LoadedFeedView.kt | 35 +++++++ .../ui/view/model/FeedListViewModel.kt | 45 +++++++++ .../compose/ui/view/state/FeedListUIState.kt | 15 +++ 11 files changed, 450 insertions(+), 28 deletions(-) create mode 100755 composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/FeedRoute.kt create mode 100755 composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/SocialPreconditionRoute.kt create mode 100644 composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/Dummy.kt create mode 100644 composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/EventListView.kt create mode 100644 composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/FeedListItem.kt create mode 100644 composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/LoadedFeedView.kt create mode 100755 composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/FeedListViewModel.kt create mode 100755 composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/state/FeedListUIState.kt 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 c1ffd679..e0113957 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,42 +1,101 @@ package ac.aux.compose.ui.composable +import ac.aux.compose.ui.composable.widgets.LoadingDataIndicator +import ac.aux.compose.ui.composable.widgets.feed.ListView +import ac.aux.compose.ui.composable.widgets.feed.LoadedFeedView import ac.aux.compose.ui.theme.AuxTheme -import androidx.compose.foundation.layout.Arrangement +import ac.aux.compose.ui.view.model.FeedListViewModel +import ac.aux.compose.ui.view.state.FeedListUIState import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth 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.Person +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +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 +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import kotlin.time.Clock +@OptIn(ExperimentalMaterial3Api::class) @Composable fun FeedScreen( - text: String + initialFeedListUIState: FeedListUIState, + onNavigateToEvent: (Event) -> Unit ) { + val feedListViewModel: FeedListViewModel = viewModel( + factory = FeedListViewModel.factory( + initialFeedListUIState = initialFeedListUIState, + ), + ) - Scaffold { innerPadding -> + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) + + Scaffold( + modifier = Modifier.nestedScroll( + scrollBehavior.nestedScrollConnection + ), + topBar = { + MediumTopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary + ), + title = { + Text( + "Aux" + ) + }, + navigationIcon = { + IconButton( + onClick = { + + } + ) { + Icon( + Icons.Default.Person, + contentDescription = "Account" + ) + } + }, + scrollBehavior = scrollBehavior + ) + + } + ) { innerPadding -> Column( modifier = Modifier.padding(innerPadding).fillMaxSize() ) { - // Notes - // Stories/Reels - // Pictures - // Private Messages (deferred to another app) - // Notifications - Column( - modifier = Modifier.padding(20.dp).fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - "\"$text\" functionality coming soon", - - ) + when(val feedListUIState = feedListViewModel.feedListUIState) { + is FeedListUIState.Loading -> { + LoadingDataIndicator() + } + FeedListUIState.Error -> { + Text("Something went wrong") + } + is FeedListUIState.Loaded -> { + LoadedFeedView( + events = feedListUIState.events, + onNavigateToEvent = onNavigateToEvent + ) + } } } } @@ -50,7 +109,8 @@ private fun FeedScreenPreview() { modifier = Modifier.padding(20.dp) ) { FeedScreen( - "Feed" + initialFeedListUIState = FeedListUIState.Loading, + onNavigateToEvent = {} ) } } diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SocialPreconditionScreen.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SocialPreconditionScreen.kt index 01cc5244..b7bc6e53 100644 --- a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SocialPreconditionScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/SocialPreconditionScreen.kt @@ -3,35 +3,110 @@ package ac.aux.compose.ui.composable import ac.aux.compose.ui.theme.AuxTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.SkipNext +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Button +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Composable fun SocialPreconditionScreen( - text: String + onNavigateToSkipForNow: () -> Unit, + onNavigateToInviteFriend: () -> Unit, + onNavigateToViewInvites: () -> Unit, ) { - Scaffold { innerPadding -> + Scaffold( + bottomBar = { + BottomAppBar( + modifier = Modifier, + actions = { + + }, + floatingActionButton = { + ExtendedFloatingActionButton( + modifier = Modifier, + icon = { + Icon( + Icons.Default.SkipNext, + contentDescription = "Skip for now" + ) + }, + text = { + Text("Skip for now") + }, + onClick = { + onNavigateToSkipForNow.invoke() + } + ) + } + ) + } + ) { innerPadding -> Column( modifier = Modifier.padding(innerPadding).fillMaxSize() ) { - // Invite A Friend - // Accept an invite Column( - modifier = Modifier.padding(20.dp).fillMaxSize(), + modifier = Modifier.fillMaxWidth().padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.spacedBy(10.dp) ) { + Text( - "\"$text\" functionality coming soon", + "Who will you be passing the aux to?", + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge ) + + Spacer( + modifier = Modifier.weight(1f) + ) + + Text( + "Tell friends to join you on Aux so your feed stays lively and fresh.", + textAlign = TextAlign.Center + ) + + Button( + onClick = { + onNavigateToInviteFriend.invoke() + } + ) { + Text( + "Invite a Friend" + ) + } + + Spacer( + modifier = Modifier.weight(1f) + ) + + Text( + "View and accept invites you may have received to stay connected with others.", + textAlign = TextAlign.Center + ) + + Button( + onClick = { + onNavigateToViewInvites.invoke() + } + ) { + Text("View invites") + } } } } @@ -45,7 +120,9 @@ private fun SocialPreconditionScreenPreview() { modifier = Modifier.padding(20.dp) ) { SocialPreconditionScreen( - "Social Precondition" + onNavigateToSkipForNow = {}, + onNavigateToInviteFriend = {}, + onNavigateToViewInvites = {} ) } } 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 ab6c6e0e..e4c700b6 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 @@ -1,9 +1,12 @@ package ac.aux.compose.ui.composable.navigation import ac.aux.compose.ui.composable.CreateProfileScreen +import ac.aux.compose.ui.composable.FeedScreen import ac.aux.compose.ui.composable.ImplementationPendingScreen import ac.aux.compose.ui.composable.LandingScreen +import ac.aux.compose.ui.composable.SocialPreconditionScreen import ac.aux.compose.ui.composable.navigation.routes.CreateProfileRoute +import ac.aux.compose.ui.composable.navigation.routes.FeedRoute import ac.aux.compose.ui.composable.navigation.routes.ImplementationPendingRoute import ac.aux.compose.ui.composable.navigation.routes.LandingRoute import androidx.compose.runtime.Composable @@ -12,6 +15,9 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.toRoute import ac.aux.compose.ui.composable.navigation.routes.Route +import ac.aux.compose.ui.composable.navigation.routes.SocialPreconditionRoute +import ac.aux.compose.ui.composable.widgets.feed.DUMMY_EVENTS +import ac.aux.compose.ui.view.state.FeedListUIState @Composable fun AuxNavHost( @@ -46,7 +52,40 @@ fun AuxNavHost( CreateProfileScreen( onNavigateToChatListRoute = { navController.navigate( - route = ImplementationPendingRoute("Feed") + route = SocialPreconditionRoute + ) + } + ) + } + composable { + SocialPreconditionScreen( + onNavigateToSkipForNow = { + navController.navigate( + route = FeedRoute + ) + }, + onNavigateToInviteFriend = { + navController.navigate( + route = ImplementationPendingRoute("Invite Friend") + ) + }, + onNavigateToViewInvites = { + navController.navigate( + route = ImplementationPendingRoute("View invites") + ) + } + ) + } + composable { + FeedScreen( + initialFeedListUIState = FeedListUIState.Loaded( + events = DUMMY_EVENTS, + ), + onNavigateToEvent = { event -> + navController.navigate( + route = ImplementationPendingRoute( + event.content + ) ) } ) diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/FeedRoute.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/FeedRoute.kt new file mode 100755 index 00000000..3d357c7d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/FeedRoute.kt @@ -0,0 +1,6 @@ +package ac.aux.compose.ui.composable.navigation.routes + +import kotlinx.serialization.Serializable + +@Serializable +object FeedRoute: Route() \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/SocialPreconditionRoute.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/SocialPreconditionRoute.kt new file mode 100755 index 00000000..5895a6b8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/navigation/routes/SocialPreconditionRoute.kt @@ -0,0 +1,6 @@ +package ac.aux.compose.ui.composable.navigation.routes + +import kotlinx.serialization.Serializable + +@Serializable +object SocialPreconditionRoute: Route() \ No newline at end of file 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 new file mode 100644 index 00000000..b67d8e71 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/Dummy.kt @@ -0,0 +1,35 @@ +package ac.aux.compose.ui.composable.widgets.feed + +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 new file mode 100644 index 00000000..f8dc3ec8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/EventListView.kt @@ -0,0 +1,85 @@ +package ac.aux.compose.ui.composable.widgets.feed + +import ac.aux.compose.ui.theme.AuxTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.material3.AssistChip +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.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() { + when (kind) { + // TODO: Support other kinds... + else -> { + Column( + modifier = Modifier.padding(10.dp).fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(10.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + 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.toString() + ) + }, + onClick = { + + } + ) + } + } + } + } + } +} + +@Preview +@Composable +private fun EventListViewPreview() { + AuxTheme { + Surface( + modifier = Modifier.padding(20.dp) + ) { + DUMMY_EVENTS.first().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 new file mode 100644 index 00000000..71b3a7fa --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/FeedListItem.kt @@ -0,0 +1,19 @@ +package ac.aux.compose.ui.composable.widgets.feed + +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 +) { + ElevatedCard( + onClick = { + onNavigateToEvent.invoke(event) + } + ) { + event.ListView() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/LoadedFeedView.kt b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/LoadedFeedView.kt new file mode 100644 index 00000000..2fd07d05 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/composable/widgets/feed/LoadedFeedView.kt @@ -0,0 +1,35 @@ +package ac.aux.compose.ui.composable.widgets.feed + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.quartz.nip01Core.core.Event + +@Composable +fun LoadedFeedView( + events: List, + onNavigateToEvent: (Event) -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(10.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + items( + items = events, + key = { e -> e.id } + ) { event -> + FeedListItem( + event = event, + onNavigateToEvent = { + onNavigateToEvent.invoke(event) + } + ) + } + } +} \ 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 new file mode 100755 index 00000000..482f325e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/model/FeedListViewModel.kt @@ -0,0 +1,45 @@ +package ac.aux.compose.ui.view.model + +import ac.aux.compose.ui.view.state.FeedListUIState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import kotlinx.coroutines.launch + +class FeedListViewModel( + initialFeedListUIState: FeedListUIState, +): ViewModel() { + var feedListUIState: FeedListUIState by mutableStateOf(initialFeedListUIState) + private set + + fun retryLoad() { + feedListUIState = FeedListUIState.Loading + loadRecipientListData() + } + + fun loadRecipientListData() { + viewModelScope.launch { + // TODO: Load Dummy data... + } + } + + + companion object { + const val TAG = "RecipientListViewModel" + + fun factory( + initialFeedListUIState: FeedListUIState = FeedListUIState.Loading, + ): ViewModelProvider.Factory = viewModelFactory { + initializer { + FeedListViewModel( + initialFeedListUIState = initialFeedListUIState, + ) + } + } + } +} \ No newline at end of file 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 new file mode 100755 index 00000000..feedde32 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/aux/compose/ui/view/state/FeedListUIState.kt @@ -0,0 +1,15 @@ +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 + +sealed interface FeedListUIState { + data class Loaded( + val events: List + ): FeedListUIState + + data object Error: FeedListUIState + data object Loading: FeedListUIState +} +