- Adds a cache for reply levels when viewing threads.

- Moves the lazy list state to the view model to saving the scrolling position when navigating between multiple threads.
This commit is contained in:
Vitor Pamplona 2024-11-01 10:10:05 -04:00
parent a825f575de
commit 91748d5c39
2 changed files with 82 additions and 33 deletions

View File

@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ThreadLevelCalculator
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter
import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter
@ -53,10 +54,21 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileGalleryFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.FeedState
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
import com.vitorpamplona.quartz.events.ChatroomKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
class NostrChannelFeedViewModel(
@ -86,7 +98,7 @@ class NostrChatroomFeedViewModel(
class NostrThreadFeedViewModel(
account: Account,
noteId: String,
) : FeedViewModel(ThreadFeedFilter(account, noteId)) {
) : LevelFeedViewModel(ThreadFeedFilter(account, noteId)) {
class Factory(
val account: Account,
val noteId: String,
@ -257,6 +269,44 @@ class NostrUserAppRecommendationsFeedViewModel(
}
}
abstract class LevelFeedViewModel(
localFilter: FeedFilter<Note>,
) : FeedViewModel(localFilter) {
var llState: LazyListState by mutableStateOf(LazyListState(0, 0))
// val cachedLevels = mutableMapOf<Note, MutableStateFlow<Int>>()
@OptIn(ExperimentalCoroutinesApi::class)
val levelCacheFlow: StateFlow<Map<Note, Int>> =
feedState.feedContent
.transformLatest { feed ->
emitAll(
if (feed is FeedState.Loaded) {
feed.feed.map {
val cache = mutableMapOf<Note, Int>()
it.list.forEach {
ThreadLevelCalculator.replyLevel(it, cache)
}
cache
}
} else {
MutableStateFlow(mapOf())
},
)
}.flowOn(Dispatchers.Default)
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
mapOf(),
)
fun levelFlowForItem(note: Note) =
levelCacheFlow
.map {
it[note] ?: 0
}.distinctUntilChanged()
}
@Stable
abstract class FeedViewModel(
localFilter: FeedFilter<Note>,
@ -272,8 +322,6 @@ abstract class FeedViewModel(
override fun invalidateData(ignoreIfDoing: Boolean) = feedState.invalidateData(ignoreIfDoing)
var llState: LazyListState by mutableStateOf(LazyListState(0, 0))
private var collectorJob: Job? = null
init {

View File

@ -47,6 +47,7 @@ import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -74,7 +75,6 @@ import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ThreadLevelCalculator
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.components.InlineCarrousel
import com.vitorpamplona.amethyst.ui.components.LoadNote
@ -141,7 +141,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent
import com.vitorpamplona.amethyst.ui.note.types.RenderTorrent
import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.LevelFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RenderFeedState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelHeader
@ -198,6 +198,7 @@ import com.vitorpamplona.quartz.events.VideoEvent
import com.vitorpamplona.quartz.events.WikiNoteEvent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@ -205,7 +206,7 @@ import kotlinx.coroutines.withContext
@Composable
fun ThreadFeedView(
noteId: String,
viewModel: FeedViewModel,
viewModel: LevelFeedViewModel,
accountViewModel: AccountViewModel,
nav: INav,
) {
@ -217,7 +218,7 @@ fun ThreadFeedView(
nav = nav,
routeForLastRead = null,
onLoaded = {
RenderThreadFeed(noteId, it, viewModel.llState, accountViewModel, nav)
RenderThreadFeed(noteId, it, viewModel.llState, viewModel::levelFlowForItem, accountViewModel, nav)
},
)
}
@ -228,6 +229,7 @@ fun RenderThreadFeed(
noteId: String,
loaded: FeedState.Loaded,
listState: LazyListState,
createLevelFlow: (Note) -> Flow<Int>,
accountViewModel: AccountViewModel,
nav: INav,
) {
@ -266,20 +268,27 @@ fun RenderThreadFeed(
state = listState,
) {
itemsIndexed(items.list, key = { _, item -> item.idHex }) { index, item ->
val level = createLevelFlow(item).collectAsStateWithLifecycle(0)
val modifier =
Modifier
.drawReplyLevel(
note = item,
level = level,
color = MaterialTheme.colorScheme.placeholderText,
selected =
if (item.idHex == noteId) {
MaterialTheme.colorScheme.lessImportantLink
} else {
MaterialTheme.colorScheme.placeholderText
},
)
if (index == 0) {
ProvideTextStyle(TextStyle(fontSize = 18.sp, lineHeight = 1.20.em)) {
NoteMaster(
item,
modifier =
Modifier.drawReplyLevel(
ThreadLevelCalculator.replyLevel(item),
MaterialTheme.colorScheme.placeholderText,
if (item.idHex == noteId) {
MaterialTheme.colorScheme.lessImportantLink
} else {
MaterialTheme.colorScheme.placeholderText
},
),
baseNote = item,
modifier = modifier,
accountViewModel = accountViewModel,
nav = nav,
)
@ -292,17 +301,8 @@ fun RenderThreadFeed(
}
NoteCompose(
item,
modifier =
Modifier.drawReplyLevel(
ThreadLevelCalculator.replyLevel(item),
MaterialTheme.colorScheme.placeholderText,
if (item.idHex == noteId) {
MaterialTheme.colorScheme.lessImportantLink
} else {
MaterialTheme.colorScheme.placeholderText
},
),
baseNote = item,
modifier = modifier,
isBoostedNote = false,
unPackReply = false,
quotesLeft = 3,
@ -321,7 +321,8 @@ fun RenderThreadFeed(
// Creates a Zebra pattern where each bar is a reply level.
fun Modifier.drawReplyLevel(
level: Int,
note: Note,
level: State<Int>,
color: Color,
selected: Color,
): Modifier =
@ -335,9 +336,9 @@ fun Modifier.drawReplyLevel(
val strokeWidth = strokeWidthDp.dp.toPx()
val levelWidth = levelWidthDp.dp.toPx()
repeat(level) {
repeat(level.value) {
this.drawLine(
if (it == level - 1) selected else color,
if (it == level.value - 1) selected else color,
Offset(padding + it * levelWidth, 0f),
Offset(padding + it * levelWidth, size.height),
strokeWidth = strokeWidth,
@ -345,7 +346,7 @@ fun Modifier.drawReplyLevel(
}
return@drawBehind
}.padding(start = (2 + (level * 3)).dp)
}.padding(start = (2 + (level.value * 3)).dp)
@OptIn(ExperimentalFoundationApi::class)
@Composable