Merge remote-tracking branch 'origin/profiles-list-management' into profiles-list-management
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
+31
-30
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user