diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt index 3c86c6e97..209b50cec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ThreadLevelCalculator.kt @@ -118,11 +118,17 @@ object ThreadLevelCalculator { if ( note.event is RepostEvent || note.event is GenericRepostEvent || replyTo == null || replyTo.isEmpty() ) { + cachedLevels[note] = 0 return 0 } - return replyTo.maxOf { - cachedLevels[it] ?: replyLevel(it, cachedLevels).apply { cachedLevels.put(it, this) } - } + 1 + val thisLevel = + replyTo.maxOf { + cachedLevels[it] ?: replyLevel(it, cachedLevels) + } + 1 + + cachedLevels[note] = thisLevel + + return thisLevel } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index b7bf8e7c0..8b824a3ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -428,7 +428,7 @@ fun ClickableNote( content: @Composable () -> Unit, ) { val updatedModifier = - remember(baseNote, backgroundColor.value) { + remember(baseNote, backgroundColor.value, modifier) { modifier .combinedClickable( onClick = { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 10a06dd42..e6ce44535 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -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 @@ -54,11 +55,22 @@ 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.amethyst.ui.screen.loggedIn.lists.FollowSetFeedViewModel 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( @@ -88,7 +100,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, @@ -269,6 +281,44 @@ class NostrUserAppRecommendationsFeedViewModel( } } +abstract class LevelFeedViewModel( + localFilter: FeedFilter, +) : FeedViewModel(localFilter) { + var llState: LazyListState by mutableStateOf(LazyListState(0, 0)) + + // val cachedLevels = mutableMapOf>() + + @OptIn(ExperimentalCoroutinesApi::class) + val levelCacheFlow: StateFlow> = + feedState.feedContent + .transformLatest { feed -> + emitAll( + if (feed is FeedState.Loaded) { + feed.feed.map { + val cache = mutableMapOf() + 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, @@ -284,8 +334,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 { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index a496b2233..83dcfb017 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -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, 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, 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 diff --git a/amethyst/src/main/res/values-fr/strings.xml b/amethyst/src/main/res/values-fr/strings.xml index a97395c19..03eb6590d 100644 --- a/amethyst/src/main/res/values-fr/strings.xml +++ b/amethyst/src/main/res/values-fr/strings.xml @@ -371,6 +371,7 @@ Non Liste des Abonnés Tous les abonnements + Autour de moi Général Liste en Sourdine Le port par défaut est 9050