Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management

This commit is contained in:
KotlinGeekDev
2024-11-01 22:28:20 +01:00
5 changed files with 93 additions and 37 deletions
@@ -118,11 +118,17 @@ object ThreadLevelCalculator {
if ( if (
note.event is RepostEvent || note.event is GenericRepostEvent || replyTo == null || replyTo.isEmpty() note.event is RepostEvent || note.event is GenericRepostEvent || replyTo == null || replyTo.isEmpty()
) { ) {
cachedLevels[note] = 0
return 0 return 0
} }
return replyTo.maxOf { val thisLevel =
cachedLevels[it] ?: replyLevel(it, cachedLevels).apply { cachedLevels.put(it, this) } replyTo.maxOf {
} + 1 cachedLevels[it] ?: replyLevel(it, cachedLevels)
} + 1
cachedLevels[note] = thisLevel
return thisLevel
} }
} }
@@ -428,7 +428,7 @@ fun ClickableNote(
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val updatedModifier = val updatedModifier =
remember(baseNote, backgroundColor.value) { remember(baseNote, backgroundColor.value, modifier) {
modifier modifier
.combinedClickable( .combinedClickable(
onClick = { onClick = {
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ThreadLevelCalculator
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter
import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter 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.UserProfileNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState 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.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetFeedViewModel
import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ChatroomKey
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job 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 import kotlinx.coroutines.launch
class NostrChannelFeedViewModel( class NostrChannelFeedViewModel(
@@ -88,7 +100,7 @@ class NostrChatroomFeedViewModel(
class NostrThreadFeedViewModel( class NostrThreadFeedViewModel(
account: Account, account: Account,
noteId: String, noteId: String,
) : FeedViewModel(ThreadFeedFilter(account, noteId)) { ) : LevelFeedViewModel(ThreadFeedFilter(account, noteId)) {
class Factory( class Factory(
val account: Account, val account: Account,
val noteId: String, val noteId: String,
@@ -269,6 +281,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 @Stable
abstract class FeedViewModel( abstract class FeedViewModel(
localFilter: FeedFilter<Note>, localFilter: FeedFilter<Note>,
@@ -284,8 +334,6 @@ abstract class FeedViewModel(
override fun invalidateData(ignoreIfDoing: Boolean) = feedState.invalidateData(ignoreIfDoing) override fun invalidateData(ignoreIfDoing: Boolean) = feedState.invalidateData(ignoreIfDoing)
var llState: LazyListState by mutableStateOf(LazyListState(0, 0))
private var collectorJob: Job? = null private var collectorJob: Job? = null
init { init {
@@ -47,6 +47,7 @@ import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -74,7 +75,6 @@ import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note 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.GenericLoadable
import com.vitorpamplona.amethyst.ui.components.InlineCarrousel import com.vitorpamplona.amethyst.ui.components.InlineCarrousel
import com.vitorpamplona.amethyst.ui.components.LoadNote 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.RenderTorrent
import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment
import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay 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.RenderFeedState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chatrooms.ChannelHeader 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 com.vitorpamplona.quartz.events.WikiNoteEvent
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -205,7 +206,7 @@ import kotlinx.coroutines.withContext
@Composable @Composable
fun ThreadFeedView( fun ThreadFeedView(
noteId: String, noteId: String,
viewModel: FeedViewModel, viewModel: LevelFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
@@ -217,7 +218,7 @@ fun ThreadFeedView(
nav = nav, nav = nav,
routeForLastRead = null, routeForLastRead = null,
onLoaded = { 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, noteId: String,
loaded: FeedState.Loaded, loaded: FeedState.Loaded,
listState: LazyListState, listState: LazyListState,
createLevelFlow: (Note) -> Flow<Int>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
@@ -266,20 +268,27 @@ fun RenderThreadFeed(
state = listState, state = listState,
) { ) {
itemsIndexed(items.list, key = { _, item -> item.idHex }) { index, item -> 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) { if (index == 0) {
ProvideTextStyle(TextStyle(fontSize = 18.sp, lineHeight = 1.20.em)) { ProvideTextStyle(TextStyle(fontSize = 18.sp, lineHeight = 1.20.em)) {
NoteMaster( NoteMaster(
item, baseNote = item,
modifier = modifier = modifier,
Modifier.drawReplyLevel(
ThreadLevelCalculator.replyLevel(item),
MaterialTheme.colorScheme.placeholderText,
if (item.idHex == noteId) {
MaterialTheme.colorScheme.lessImportantLink
} else {
MaterialTheme.colorScheme.placeholderText
},
),
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) )
@@ -292,17 +301,8 @@ fun RenderThreadFeed(
} }
NoteCompose( NoteCompose(
item, baseNote = item,
modifier = modifier = modifier,
Modifier.drawReplyLevel(
ThreadLevelCalculator.replyLevel(item),
MaterialTheme.colorScheme.placeholderText,
if (item.idHex == noteId) {
MaterialTheme.colorScheme.lessImportantLink
} else {
MaterialTheme.colorScheme.placeholderText
},
),
isBoostedNote = false, isBoostedNote = false,
unPackReply = false, unPackReply = false,
quotesLeft = 3, quotesLeft = 3,
@@ -321,7 +321,8 @@ fun RenderThreadFeed(
// Creates a Zebra pattern where each bar is a reply level. // Creates a Zebra pattern where each bar is a reply level.
fun Modifier.drawReplyLevel( fun Modifier.drawReplyLevel(
level: Int, note: Note,
level: State<Int>,
color: Color, color: Color,
selected: Color, selected: Color,
): Modifier = ): Modifier =
@@ -335,9 +336,9 @@ fun Modifier.drawReplyLevel(
val strokeWidth = strokeWidthDp.dp.toPx() val strokeWidth = strokeWidthDp.dp.toPx()
val levelWidth = levelWidthDp.dp.toPx() val levelWidth = levelWidthDp.dp.toPx()
repeat(level) { repeat(level.value) {
this.drawLine( 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, 0f),
Offset(padding + it * levelWidth, size.height), Offset(padding + it * levelWidth, size.height),
strokeWidth = strokeWidth, strokeWidth = strokeWidth,
@@ -345,7 +346,7 @@ fun Modifier.drawReplyLevel(
} }
return@drawBehind return@drawBehind
}.padding(start = (2 + (level * 3)).dp) }.padding(start = (2 + (level.value * 3)).dp)
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -371,6 +371,7 @@
<string name="no">Non</string> <string name="no">Non</string>
<string name="follow_list_selection">Liste des Abonnés</string> <string name="follow_list_selection">Liste des Abonnés</string>
<string name="follow_list_kind3follows">Tous les abonnements</string> <string name="follow_list_kind3follows">Tous les abonnements</string>
<string name="follow_list_aroundme">Autour de moi</string>
<string name="follow_list_global">Général</string> <string name="follow_list_global">Général</string>
<string name="follow_list_mute_list">Liste en Sourdine</string> <string name="follow_list_mute_list">Liste en Sourdine</string>
<string name="connect_through_your_orbot_setup_short">Le port par défaut est 9050</string> <string name="connect_through_your_orbot_setup_short">Le port par défaut est 9050</string>