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