Show something on the feed

This commit is contained in:
Kgothatso Ngako
2026-03-23 15:39:40 +02:00
parent ec9db509cf
commit 3e9566d0e9
11 changed files with 450 additions and 28 deletions

View File

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

View File

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

View File

@@ -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<SocialPreconditionRoute> {
SocialPreconditionScreen(
onNavigateToSkipForNow = {
navController.navigate(
route = FeedRoute
)
},
onNavigateToInviteFriend = {
navController.navigate(
route = ImplementationPendingRoute("Invite Friend")
)
},
onNavigateToViewInvites = {
navController.navigate(
route = ImplementationPendingRoute("View invites")
)
}
)
}
composable<FeedRoute> {
FeedScreen(
initialFeedListUIState = FeedListUIState.Loaded(
events = DUMMY_EVENTS,
),
onNavigateToEvent = { event ->
navController.navigate(
route = ImplementationPendingRoute(
event.content
)
)
}
)

View File

@@ -0,0 +1,6 @@
package ac.aux.compose.ui.composable.navigation.routes
import kotlinx.serialization.Serializable
@Serializable
object FeedRoute: Route()

View File

@@ -0,0 +1,6 @@
package ac.aux.compose.ui.composable.navigation.routes
import kotlinx.serialization.Serializable
@Serializable
object SocialPreconditionRoute: Route()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Event>
): FeedListUIState
data object Error: FeedListUIState
data object Loading: FeedListUIState
}