No more blinking in Feeds

This commit is contained in:
Vitor Pamplona
2023-01-27 19:28:59 -03:00
parent ae82c690ea
commit 1e3654396b
13 changed files with 176 additions and 96 deletions

View File

@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.screen package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.MutableState
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
abstract class Card() { abstract class Card() {
@ -37,7 +38,7 @@ class BoostSetCard(val note: Note, val boostEvents: List<Note>): Card() {
sealed class CardFeedState { sealed class CardFeedState {
object Loading: CardFeedState() object Loading: CardFeedState()
class Loaded(val feed: List<Card>): CardFeedState() class Loaded(val feed: MutableState<List<Card>>): CardFeedState()
object Empty: CardFeedState() object Empty: CardFeedState()
class FeedError(val errorMessage: String): CardFeedState() class FeedError(val errorMessage: String): CardFeedState()
} }

View File

@ -4,6 +4,7 @@ import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
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.runtime.Composable import androidx.compose.runtime.Composable
@ -30,8 +31,6 @@ fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewMode
var isRefreshing by remember { mutableStateOf(false) } var isRefreshing by remember { mutableStateOf(false) }
val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) val swipeRefreshState = rememberSwipeRefreshState(isRefreshing)
val listState = rememberLazyListState()
LaunchedEffect(isRefreshing) { LaunchedEffect(isRefreshing) {
if (isRefreshing) { if (isRefreshing) {
viewModel.refresh() viewModel.refresh()
@ -59,21 +58,12 @@ fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewMode
} }
} }
is CardFeedState.Loaded -> { is CardFeedState.Loaded -> {
LazyColumn( FeedLoaded(
contentPadding = PaddingValues( state,
top = 10.dp, accountViewModel,
bottom = 10.dp navController,
), routeForLastRead
state = listState )
) {
itemsIndexed(state.feed, key = { _, item -> item.id() }) { index, item ->
when (item) {
is NoteCard -> NoteCompose(item.note, isInnerNote = false, accountViewModel = accountViewModel, navController = navController, routeForLastRead = routeForLastRead)
is LikeSetCard -> LikeSetCompose(item, isInnerNote = false, accountViewModel = accountViewModel, navController = navController, routeForLastRead = routeForLastRead)
is BoostSetCard -> BoostSetCompose(item, isInnerNote = false, accountViewModel = accountViewModel, navController = navController, routeForLastRead = routeForLastRead)
}
}
}
} }
CardFeedState.Loading -> { CardFeedState.Loading -> {
LoadingFeed() LoadingFeed()
@ -83,3 +73,47 @@ fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewMode
} }
} }
} }
@Composable
private fun FeedLoaded(
state: CardFeedState.Loaded,
accountViewModel: AccountViewModel,
navController: NavController,
routeForLastRead: String
) {
val listState = rememberLazyListState()
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp,
bottom = 10.dp
),
state = listState
) {
itemsIndexed(state.feed.value, key = { _, item -> item.id() }) { index, item ->
when (item) {
is NoteCard -> NoteCompose(
item.note,
isInnerNote = false,
accountViewModel = accountViewModel,
navController = navController,
routeForLastRead = routeForLastRead
)
is LikeSetCard -> LikeSetCompose(
item,
isInnerNote = false,
accountViewModel = accountViewModel,
navController = navController,
routeForLastRead = routeForLastRead
)
is BoostSetCard -> BoostSetCompose(
item,
isInnerNote = false,
accountViewModel = accountViewModel,
navController = navController,
routeForLastRead = routeForLastRead
)
}
}
}
}

View File

@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
@ -43,7 +44,7 @@ class CardFeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel() {
val newCards = convertToCard(notes.minus(lastNotesCopy)) val newCards = convertToCard(notes.minus(lastNotesCopy))
if (newCards.isNotEmpty()) { if (newCards.isNotEmpty()) {
lastNotes = notes lastNotes = notes
updateFeed((oldNotesState.feed + newCards).sortedBy { it.createdAt() }.reversed()) updateFeed((oldNotesState.feed.value + newCards).sortedBy { it.createdAt() }.reversed())
} }
} else { } else {
val cards = convertToCard(notes) val cards = convertToCard(notes)
@ -83,14 +84,20 @@ class CardFeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel() {
fun updateFeed(notes: List<Card>) { fun updateFeed(notes: List<Card>) {
val scope = CoroutineScope(Job() + Dispatchers.Main) val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch { scope.launch {
val currentState = feedContent.value
if (notes.isEmpty()) { if (notes.isEmpty()) {
_feedContent.update { CardFeedState.Empty } _feedContent.update { CardFeedState.Empty }
} else if (currentState is CardFeedState.Loaded) {
// updates the current list
currentState.feed.value = notes
} else { } else {
_feedContent.update { CardFeedState.Loaded(notes) } _feedContent.update { CardFeedState.Loaded(mutableStateOf(notes)) }
} }
} }
} }
var handlerWaiting = false var handlerWaiting = false
fun invalidateData() { fun invalidateData() {
synchronized(handlerWaiting) { synchronized(handlerWaiting) {

View File

@ -25,7 +25,6 @@ fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewMode
val feedState by viewModel.feedContent.collectAsState() val feedState by viewModel.feedContent.collectAsState()
var isRefreshing by remember { mutableStateOf(false) } var isRefreshing by remember { mutableStateOf(false) }
val swipeRefreshState = rememberSwipeRefreshState(isRefreshing)
val listState = rememberLazyListState() val listState = rememberLazyListState()
@ -36,43 +35,36 @@ fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewMode
} }
} }
SwipeRefresh( Column() {
state = swipeRefreshState, Crossfade(targetState = feedState) { state ->
onRefresh = { when (state) {
isRefreshing = true is FeedState.Empty -> {
}, FeedEmpty {
) { isRefreshing = true
Column() { }
Crossfade(targetState = feedState) { state -> }
when (state) { is FeedState.FeedError -> {
is FeedState.Empty -> { FeedError(state.errorMessage) {
FeedEmpty { isRefreshing = true
isRefreshing = true }
}
is FeedState.Loaded -> {
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp,
bottom = 10.dp
),
reverseLayout = true,
state = listState
) {
var previousDate: String = ""
itemsIndexed(state.feed.value, key = { index, item -> if (index == 0) index else item.idHex }) { index, item ->
ChatroomMessageCompose(item, routeForLastRead, accountViewModel = accountViewModel, navController = navController)
} }
} }
is FeedState.FeedError -> { }
FeedError(state.errorMessage) { FeedState.Loading -> {
isRefreshing = true LoadingFeed()
}
}
is FeedState.Loaded -> {
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp,
bottom = 10.dp
),
reverseLayout = true,
state = listState
) {
var previousDate: String = ""
itemsIndexed(state.feed, key = { index, item -> if (index == 0) index else item.idHex }) { index, item ->
ChatroomMessageCompose(item, routeForLastRead, accountViewModel = accountViewModel, navController = navController)
}
}
}
FeedState.Loading -> {
LoadingFeed()
}
} }
} }
} }

View File

@ -68,7 +68,7 @@ fun ChatroomListFeedView(viewModel: FeedViewModel, accountViewModel: AccountView
), ),
state = listState state = listState
) { ) {
itemsIndexed(state.feed, key = { index, item -> item.idHex }) { index, item -> itemsIndexed(state.feed.value, key = { index, item -> item.idHex }) { index, item ->
ChatroomCompose(item, accountViewModel = accountViewModel, navController = navController) ChatroomCompose(item, accountViewModel = accountViewModel, navController = navController)
} }
} }

View File

@ -1,11 +1,12 @@
package com.vitorpamplona.amethyst.ui.screen package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.MutableState
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
sealed class FeedState { sealed class FeedState {
object Loading : FeedState() object Loading : FeedState()
class Loaded(val feed: List<Note>) : FeedState() class Loaded(val feed: MutableState<List<Note>>) : FeedState()
object Empty : FeedState() object Empty : FeedState()
class FeedError(val errorMessage: String) : FeedState() class FeedError(val errorMessage: String) : FeedState()
} }

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
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.material.Button import androidx.compose.material.Button
@ -40,8 +41,6 @@ fun FeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navCo
var isRefreshing by remember { mutableStateOf(false) } var isRefreshing by remember { mutableStateOf(false) }
val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) val swipeRefreshState = rememberSwipeRefreshState(isRefreshing)
val listState = rememberLazyListState()
LaunchedEffect(isRefreshing) { LaunchedEffect(isRefreshing) {
if (isRefreshing) { if (isRefreshing) {
viewModel.hardRefresh() viewModel.hardRefresh()
@ -49,12 +48,15 @@ fun FeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navCo
} }
} }
println("FeedView Refresh ${feedState}")
SwipeRefresh( SwipeRefresh(
state = swipeRefreshState, state = swipeRefreshState,
onRefresh = { onRefresh = {
isRefreshing = true isRefreshing = true
}, },
) { ) {
Column() { Column() {
Crossfade(targetState = feedState) { state -> Crossfade(targetState = feedState) { state ->
when (state) { when (state) {
@ -69,22 +71,12 @@ fun FeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navCo
} }
} }
is FeedState.Loaded -> { is FeedState.Loaded -> {
LazyColumn( FeedLoaded(
contentPadding = PaddingValues( state,
top = 10.dp, routeForLastRead,
bottom = 10.dp accountViewModel,
), navController
state = listState )
) {
itemsIndexed(state.feed, key = { _, item -> item.idHex }) { index, item ->
NoteCompose(item,
isInnerNote = false,
routeForLastRead = routeForLastRead,
accountViewModel = accountViewModel,
navController = navController
)
}
}
} }
FeedState.Loading -> { FeedState.Loading -> {
LoadingFeed() LoadingFeed()
@ -95,6 +87,34 @@ fun FeedView(viewModel: FeedViewModel, accountViewModel: AccountViewModel, navCo
} }
} }
@Composable
private fun FeedLoaded(
state: FeedState.Loaded,
routeForLastRead: String?,
accountViewModel: AccountViewModel,
navController: NavController
) {
val listState = rememberLazyListState()
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp,
bottom = 10.dp
),
state = listState
) {
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { index, item ->
NoteCompose(
item,
isInnerNote = false,
routeForLastRead = routeForLastRead,
accountViewModel = accountViewModel,
navController = navController
)
}
}
}
@Composable @Composable
fun LoadingFeed() { fun LoadingFeed() {
Column( Column(

View File

@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.ui.screen package com.vitorpamplona.amethyst.ui.screen
import android.util.Log import android.util.Log
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -97,10 +98,15 @@ abstract class FeedViewModel(val dataSource: NostrDataSource<Note>): ViewModel()
} }
fun updateFeed(notes: List<Note>) { fun updateFeed(notes: List<Note>) {
val currentState = feedContent.value
if (notes.isEmpty()) { if (notes.isEmpty()) {
_feedContent.update { FeedState.Empty } _feedContent.update { FeedState.Empty }
} else if (currentState is FeedState.Loaded) {
// updates the current list
currentState.feed.value = notes
} else { } else {
_feedContent.update { FeedState.Loaded(notes) } _feedContent.update { FeedState.Loaded(mutableStateOf(notes)) }
} }
} }

View File

@ -88,9 +88,9 @@ fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: A
listState.animateScrollToItem(noteIdPositionInThread, 0) listState.animateScrollToItem(noteIdPositionInThread, 0)
} }
val notePosition = state.feed.filter { it.idHex == noteId}.firstOrNull() val notePosition = state.feed.value.filter { it.idHex == noteId}.firstOrNull()
if (notePosition != null) { if (notePosition != null) {
noteIdPositionInThread = state.feed.indexOf(notePosition) noteIdPositionInThread = state.feed.value.indexOf(notePosition)
} }
LazyColumn( LazyColumn(
@ -100,7 +100,7 @@ fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: A
), ),
state = listState state = listState
) { ) {
itemsIndexed(state.feed, key = { _, item -> item.idHex }) { index, item -> itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { index, item ->
if (index == 0) if (index == 0)
NoteMaster(item, accountViewModel = accountViewModel, navController = navController) NoteMaster(item, accountViewModel = accountViewModel, navController = navController)
else { else {

View File

@ -1,10 +1,11 @@
package com.vitorpamplona.amethyst.ui.screen package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.MutableState
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
sealed class UserFeedState { sealed class UserFeedState {
object Loading : UserFeedState() object Loading : UserFeedState()
class Loaded(val feed: List<User>) : UserFeedState() class Loaded(val feed: MutableState<List<User>>) : UserFeedState()
object Empty : UserFeedState() object Empty : UserFeedState()
class FeedError(val errorMessage: String) : UserFeedState() class FeedError(val errorMessage: String) : UserFeedState()
} }

View File

@ -4,6 +4,7 @@ import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
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.runtime.Composable import androidx.compose.runtime.Composable
@ -28,8 +29,6 @@ fun UserFeedView(viewModel: UserFeedViewModel, accountViewModel: AccountViewMode
var isRefreshing by remember { mutableStateOf(false) } var isRefreshing by remember { mutableStateOf(false) }
val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) val swipeRefreshState = rememberSwipeRefreshState(isRefreshing)
val listState = rememberLazyListState()
LaunchedEffect(isRefreshing) { LaunchedEffect(isRefreshing) {
if (isRefreshing) { if (isRefreshing) {
viewModel.refresh() viewModel.refresh()
@ -57,17 +56,7 @@ fun UserFeedView(viewModel: UserFeedViewModel, accountViewModel: AccountViewMode
} }
} }
is UserFeedState.Loaded -> { is UserFeedState.Loaded -> {
LazyColumn( FeedLoaded(state, accountViewModel, navController)
contentPadding = PaddingValues(
top = 10.dp,
bottom = 10.dp
),
state = listState
) {
itemsIndexed(state.feed, key = { _, item -> item.pubkeyHex }) { index, item ->
UserCompose(item, accountViewModel = accountViewModel, navController = navController)
}
}
} }
UserFeedState.Loading -> { UserFeedState.Loading -> {
LoadingFeed() LoadingFeed()
@ -77,3 +66,24 @@ fun UserFeedView(viewModel: UserFeedViewModel, accountViewModel: AccountViewMode
} }
} }
} }
@Composable
private fun FeedLoaded(
state: UserFeedState.Loaded,
accountViewModel: AccountViewModel,
navController: NavController
) {
val listState = rememberLazyListState()
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp,
bottom = 10.dp
),
state = listState
) {
itemsIndexed(state.feed.value, key = { _, item -> item.pubkeyHex }) { index, item ->
UserCompose(item, accountViewModel = accountViewModel, navController = navController)
}
}
}

View File

@ -2,10 +2,12 @@ package com.vitorpamplona.amethyst.ui.screen
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.LocalCacheState
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrDataSource import com.vitorpamplona.amethyst.service.NostrDataSource
import com.vitorpamplona.amethyst.service.NostrHiddenAccountsDataSource import com.vitorpamplona.amethyst.service.NostrHiddenAccountsDataSource
@ -60,10 +62,15 @@ open class UserFeedViewModel(val dataSource: NostrDataSource<User>): ViewModel()
fun updateFeed(notes: List<User>) { fun updateFeed(notes: List<User>) {
val scope = CoroutineScope(Job() + Dispatchers.Main) val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch { scope.launch {
val currentState = feedContent.value
if (notes.isEmpty()) { if (notes.isEmpty()) {
_feedContent.update { UserFeedState.Empty } _feedContent.update { UserFeedState.Empty }
} else if (currentState is UserFeedState.Loaded) {
// updates the current list
currentState.feed.value = notes
} else { } else {
_feedContent.update { UserFeedState.Loaded(notes) } _feedContent.update { UserFeedState.Loaded(mutableStateOf(notes)) }
} }
} }
} }

View File

@ -50,6 +50,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@ -425,7 +426,7 @@ fun FollowButton(onClick: () -> Unit) {
backgroundColor = MaterialTheme.colors.primary backgroundColor = MaterialTheme.colors.primary
) )
) { ) {
Text(text = "Follow", color = Color.White) Text(text = "Follow", color = Color.White, textAlign = TextAlign.Center)
} }
} }