Add component for displaying the number of people in the follow list. Make some minor refactoring.
This commit is contained in:
+77
-40
@@ -32,18 +32,19 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.People
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -52,6 +53,7 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
@@ -64,7 +66,9 @@ import com.vitorpamplona.amethyst.ui.screen.NostrUserFollowSetFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
|
||||
@@ -79,26 +83,7 @@ fun ListsScreen(
|
||||
factory = NostrUserFollowSetFeedViewModel.Factory(accountViewModel.account),
|
||||
)
|
||||
|
||||
val followSetsFlow by followSetsViewModel.account.followSetNotesFlow().collectAsState()
|
||||
LaunchedEffect(followSetsFlow) {
|
||||
followSetsViewModel.invalidateData()
|
||||
}
|
||||
|
||||
CustomListsScreen(
|
||||
followSetsViewModel,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CustomListsScreen(
|
||||
followSetsViewModel: NostrUserFollowSetFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
val setsState by followSetsViewModel.feedContent.collectAsStateWithLifecycle()
|
||||
|
||||
DisposableEffect(lifeCycleOwner) {
|
||||
val observer =
|
||||
@@ -113,6 +98,31 @@ fun CustomListsScreen(
|
||||
onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) }
|
||||
}
|
||||
|
||||
val followSetsFlow by followSetsViewModel.feedContent.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(followSetsFlow) {
|
||||
followSetsViewModel.invalidateData()
|
||||
}
|
||||
|
||||
CustomListsScreen(
|
||||
followSetsFlow,
|
||||
refresh = {
|
||||
followSetsViewModel.invalidateData()
|
||||
},
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CustomListsScreen(
|
||||
followSetState: FollowSetState,
|
||||
refresh: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
// val setsState by followSetsViewModel.feedContent.collectAsStateWithLifecycle()
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = false,
|
||||
accountViewModel = accountViewModel,
|
||||
@@ -121,24 +131,29 @@ fun CustomListsScreen(
|
||||
},
|
||||
) {
|
||||
Column(Modifier.padding(it).fillMaxHeight()) {
|
||||
when (setsState) {
|
||||
FollowSetState.Empty ->
|
||||
FeedEmpty {
|
||||
followSetsViewModel.invalidateData()
|
||||
}
|
||||
is FollowSetState.FeedError ->
|
||||
FeedError(
|
||||
(setsState as FollowSetState.FeedError).errorMessage,
|
||||
) {
|
||||
followSetsViewModel.invalidateData()
|
||||
}
|
||||
when (followSetState) {
|
||||
FollowSetState.Loading -> LoadingFeed()
|
||||
|
||||
is FollowSetState.Loaded -> {
|
||||
val followSetFeed = (setsState as FollowSetState.Loaded).feed
|
||||
val followSetFeed = followSetState.feed
|
||||
FollowListLoaded(
|
||||
loadedFeedState = followSetFeed,
|
||||
onRefresh = refresh,
|
||||
)
|
||||
}
|
||||
FollowSetState.Loading -> LoadingFeed()
|
||||
|
||||
is FollowSetState.Empty -> {
|
||||
FeedEmpty {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
is FollowSetState.FeedError ->
|
||||
FeedError(
|
||||
followSetState.errorMessage,
|
||||
) {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,9 +163,11 @@ fun CustomListsScreen(
|
||||
fun FollowListLoaded(
|
||||
modifier: Modifier = Modifier,
|
||||
loadedFeedState: List<FollowSet>,
|
||||
onRefresh: () -> Unit = {},
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
Log.d("FollowSetComposable", "FollowListLoaded: Follow Set size: ${loadedFeedState.size}")
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
contentPadding = FeedPadding,
|
||||
@@ -173,17 +190,37 @@ fun CustomListItem(
|
||||
width = Dp.Hairline,
|
||||
color = Color.Gray,
|
||||
shape = RoundedCornerShape(percent = 20),
|
||||
).padding(all = 10.dp),
|
||||
).padding(all = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(followSet.title, fontWeight = FontWeight.Bold)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(followSet.title, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
FilterChip(
|
||||
selected = true,
|
||||
onClick = {},
|
||||
label = {
|
||||
Text(text = "${followSet.profileList.size}")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.People,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
Text(
|
||||
followSet.description ?: "No description for this list.",
|
||||
followSet.description ?: "",
|
||||
fontWeight = FontWeight.Light,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 3,
|
||||
)
|
||||
@@ -203,7 +240,7 @@ fun CustomListItem(
|
||||
),
|
||||
contentDescription = "Icon for $text List",
|
||||
)
|
||||
Text(text, color = Color.Gray)
|
||||
Text(text, color = Color.Gray, fontWeight = FontWeight.Light)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,7 +251,7 @@ fun CustomListItem(
|
||||
private fun ListItemPreview() {
|
||||
val sampleFollowSet =
|
||||
FollowSet(
|
||||
isPrivate = true,
|
||||
isPrivate = false,
|
||||
title = "Sample List Title",
|
||||
description = "Sample List Description",
|
||||
emptySet(),
|
||||
|
||||
+25
-20
@@ -50,46 +50,53 @@ open class FollowSetFeedViewModel(
|
||||
val feedContent = _feedContent.asStateFlow()
|
||||
|
||||
private fun refresh() {
|
||||
viewModelScope.launch { refreshSuspended() }
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
refreshSuspended()
|
||||
}
|
||||
}
|
||||
|
||||
override val isRefreshing: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
private fun refreshSuspended() {
|
||||
checkNotInMainThread()
|
||||
|
||||
try {
|
||||
isRefreshing.value = true
|
||||
val oldFeedState = _feedContent.value
|
||||
|
||||
val notes = dataSource.loadTop().toImmutableList()
|
||||
val newSets = dataSource.loadTop().toImmutableList()
|
||||
|
||||
val oldNotesState = _feedContent.value
|
||||
if (oldNotesState is FollowSetState.Loaded) {
|
||||
if (oldFeedState is FollowSetState.Loaded) {
|
||||
val oldFeedList = oldFeedState.feed.toImmutableList()
|
||||
// Using size as a proxy for has changed.
|
||||
if (!equalImmutableLists(notes, oldNotesState.feed.toImmutableList())) {
|
||||
updateFeed(notes)
|
||||
if (!equalImmutableLists(newSets, oldFeedList)) {
|
||||
updateFeed(newSets)
|
||||
}
|
||||
} else {
|
||||
updateFeed(notes)
|
||||
updateFeed(newSets)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(
|
||||
"FollowSetFeedViewModel",
|
||||
"refreshSuspended: Error loading or refreshing feed -> ${e.message}",
|
||||
)
|
||||
_feedContent.update { FollowSetState.FeedError(e.message.toString()) }
|
||||
} finally {
|
||||
isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFeed(notes: ImmutableList<FollowSet>) {
|
||||
viewModelScope.launch {
|
||||
if (notes.isEmpty()) {
|
||||
_feedContent.update { FollowSetState.Empty }
|
||||
} else {
|
||||
_feedContent.update { FollowSetState.Loaded(notes) }
|
||||
}
|
||||
private fun updateFeed(sets: ImmutableList<FollowSet>) {
|
||||
if (sets.isNotEmpty()) {
|
||||
_feedContent.update { FollowSetState.Loaded(sets) }
|
||||
} else {
|
||||
_feedContent.update { FollowSetState.Empty }
|
||||
}
|
||||
}
|
||||
|
||||
private val bundler = BundledUpdate(250)
|
||||
private val bundler = BundledUpdate(1000, Dispatchers.IO)
|
||||
|
||||
override fun invalidateData(ignoreIfDoing: Boolean) {
|
||||
// refresh()
|
||||
bundler.invalidate(ignoreIfDoing) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
@@ -101,12 +108,10 @@ open class FollowSetFeedViewModel(
|
||||
|
||||
init {
|
||||
Log.d("Init", this.javaClass.simpleName)
|
||||
Log.d(this.javaClass.simpleName, " FollowSetState : ${_feedContent.value}")
|
||||
collectorJob =
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
checkNotInMainThread()
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
checkNotInMainThread()
|
||||
|
||||
invalidateData()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user