Converts LiveData for Content-Sensitivity and Transitive Hidden Users into Flow to avoid locking the main thread while scrolling.

Requests flows on the Default thread.
This commit is contained in:
Vitor Pamplona 2024-06-21 18:38:31 -04:00
parent f7c60b3745
commit a14ab59e78
8 changed files with 88 additions and 85 deletions

View File

@ -328,10 +328,10 @@ object LocalPreferences {
)
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, account.hasDonatedInVersion)
if (account.showSensitiveContent == null) {
if (account.showSensitiveContent.value == null) {
remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
} else {
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent.value!!)
}
putString(
@ -597,7 +597,7 @@ object LocalPreferences {
backupContactList = latestContactList,
proxy = proxy,
proxyPort = proxyPort,
showSensitiveContent = showSensitiveContent,
showSensitiveContent = MutableStateFlow(showSensitiveContent),
warnAboutPostsWithReports = warnAboutReports,
filterSpamFromStrangers = filterSpam,
lastReadPerRoute = lastReadPerRoute,

View File

@ -26,7 +26,6 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.core.os.ConfigurationCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap
@ -194,7 +193,7 @@ class Account(
var backupContactList: ContactListEvent? = null,
var proxy: Proxy? = null,
var proxyPort: Int = 9050,
var showSensitiveContent: Boolean? = null,
var showSensitiveContent: MutableStateFlow<Boolean?> = MutableStateFlow(null),
var warnAboutPostsWithReports: Boolean = true,
var filterSpamFromStrangers: Boolean = true,
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>(),
@ -202,7 +201,7 @@ class Account(
var pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
val scope: CoroutineScope = Amethyst.instance.applicationIOScope,
) {
var transientHiddenUsers: Set<String> = setOf()
var transientHiddenUsers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf())
data class PaymentRequest(
val relayUrl: String,
@ -238,6 +237,8 @@ class Account(
getPrivateOutboxRelayListFlow(),
userProfile().flow().relays.stateFlow,
) { nip65RelayList, dmRelayList, searchRelayList, privateOutBox, userProfile ->
checkNotInMainThread()
val baseRelaySet = activeRelays() ?: convertLocalRelays()
val newDMRelaySet = (dmRelayList.note.event as? ChatMessageRelayListEvent)?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet()
val searchRelaySet = (searchRelayList.note.event as? SearchRelayListEvent)?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: Constants.defaultSearchRelaySet
@ -358,6 +359,7 @@ class Account(
@OptIn(ExperimentalCoroutinesApi::class)
val liveKind3FollowsFlow: Flow<LiveFollowLists> =
userProfile().flow().follows.stateFlow.transformLatest {
checkNotInMainThread()
emit(
LiveFollowLists(
it.user.cachedFollowingKeySet(),
@ -394,6 +396,7 @@ class Account(
peopleListFollowsSource: Flow<ListNameNotePair>,
): Flow<LiveFollowLists?> =
combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows ->
checkNotInMainThread()
if (peopleListFollows.listName == GLOBAL_FOLLOWS) {
emit(null)
} else if (peopleListFollows.listName == KIND3_FOLLOWS) {
@ -500,10 +503,11 @@ class Account(
val flowHiddenUsers: StateFlow<LiveHiddenUsers> by lazy {
combineTransform(
live.asFlow(),
transientHiddenUsers,
showSensitiveContent,
getBlockListNote().flow().metadata.stateFlow,
getMuteListNote().flow().metadata.stateFlow,
) { localLive, blockList, muteList ->
) { transientHiddenUsers, showSensitiveContent, blockList, muteList ->
checkNotInMainThread()
val resultBlockList =
@ -532,8 +536,8 @@ class Account(
LiveHiddenUsers(
hiddenUsers = (resultBlockList.users + resultMuteList.users),
hiddenWords = hiddenWords,
spammers = localLive.account.transientHiddenUsers,
showSensitiveContent = localLive.account.showSensitiveContent,
spammers = transientHiddenUsers,
showSensitiveContent = showSensitiveContent,
),
)
}.stateIn(
@ -542,8 +546,8 @@ class Account(
LiveHiddenUsers(
hiddenUsers = setOf(),
hiddenWords = setOf(),
spammers = transientHiddenUsers,
showSensitiveContent = showSensitiveContent,
spammers = transientHiddenUsers.value,
showSensitiveContent = showSensitiveContent.value,
),
)
}
@ -596,7 +600,9 @@ class Account(
filterSpamFromStrangers = filterSpam
LocalCache.antiSpam.active = filterSpamFromStrangers
if (!filterSpamFromStrangers) {
transientHiddenUsers = setOf()
transientHiddenUsers.update {
emptySet()
}
}
live.invalidateData()
saveable.invalidateData()
@ -2377,7 +2383,9 @@ class Account(
}
}
transientHiddenUsers = (transientHiddenUsers - pubkeyHex)
transientHiddenUsers.update {
it - pubkeyHex
}
live.invalidateData()
saveable.invalidateData()
}
@ -2889,7 +2897,9 @@ class Account(
}
fun updateShowSensitiveContent(show: Boolean?) {
showSensitiveContent = show
showSensitiveContent.update {
show
}
saveable.invalidateData()
live.invalidateData()
}
@ -2928,10 +2938,12 @@ class Account(
// imports transient blocks due to spam.
LocalCache.antiSpam.liveSpam.observeForever {
GlobalScope.launch(Dispatchers.IO) {
it.cache.spamMessages.snapshot().values.forEach {
if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) {
if (it.pubkeyHex != userProfile().pubkeyHex && it.pubkeyHex !in followingKeySet()) {
transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex)
it.cache.spamMessages.snapshot().values.forEach { spammer ->
if (spammer.pubkeyHex !in transientHiddenUsers.value && spammer.duplicatedMessages.size >= 5) {
if (spammer.pubkeyHex != userProfile().pubkeyHex && spammer.pubkeyHex !in followingKeySet()) {
transientHiddenUsers.update {
it + spammer.pubkeyHex
}
live.invalidateData()
}
}

View File

@ -179,9 +179,7 @@ object Client : RelayPool.Listener {
subscriptions = subscriptions.minus(subscriptionId)
}
fun isActive(subscriptionId: String): Boolean {
return subscriptions.contains(subscriptionId)
}
fun isActive(subscriptionId: String): Boolean = subscriptions.contains(subscriptionId)
@OptIn(DelicateCoroutinesApi::class)
override fun onEvent(
@ -205,9 +203,9 @@ object Client : RelayPool.Listener {
) {
// Releases the Web thread for the new payload.
// May need to add a processing queue if processing new events become too costly.
GlobalScope.launch(Dispatchers.Default) {
listeners.forEach { it.onRelayStateChange(type, relay, channel) }
}
// GlobalScope.launch(Dispatchers.Default) {
listeners.forEach { it.onRelayStateChange(type, relay, channel) }
// }
}
@OptIn(DelicateCoroutinesApi::class)
@ -249,21 +247,15 @@ object Client : RelayPool.Listener {
listeners = listeners.plus(listener)
}
fun isSubscribed(listener: Listener): Boolean {
return listeners.contains(listener)
}
fun isSubscribed(listener: Listener): Boolean = listeners.contains(listener)
fun unsubscribe(listener: Listener) {
listeners = listeners.minus(listener)
}
fun allSubscriptions(): Map<String, List<TypedFilter>> {
return subscriptions
}
fun allSubscriptions(): Map<String, List<TypedFilter>> = subscriptions
fun getSubscriptionFilters(subId: String): List<TypedFilter> {
return subscriptions[subId] ?: emptyList()
}
fun getSubscriptionFilters(subId: String): List<TypedFilter> = subscriptions[subId] ?: emptyList()
abstract class Listener {
/** A new message was received */

View File

@ -39,7 +39,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -50,6 +49,7 @@ 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.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
@ -97,10 +97,9 @@ fun SensitivityWarning(
accountViewModel: AccountViewModel,
content: @Composable () -> Unit,
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val accountState = accountViewModel.account.showSensitiveContent.collectAsStateWithLifecycle()
var showContentWarningNote by
remember(accountState) { mutableStateOf(accountState?.account?.showSensitiveContent != true) }
var showContentWarningNote by remember(accountState) { mutableStateOf(accountState.value != true) }
CrossfadeIfEnabled(targetState = showContentWarningNote, accountViewModel = accountViewModel) {
if (it) {
@ -118,18 +117,26 @@ fun ContentWarningNote(onDismiss: () -> Unit) {
Column(modifier = Modifier.padding(start = 10.dp)) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Box(
Modifier.height(80.dp).width(90.dp),
Modifier
.height(80.dp)
.width(90.dp),
) {
Icon(
imageVector = Icons.Default.Visibility,
contentDescription = stringRes(R.string.content_warning),
modifier = Modifier.size(70.dp).align(Alignment.BottomStart),
modifier =
Modifier
.size(70.dp)
.align(Alignment.BottomStart),
tint = MaterialTheme.colorScheme.onBackground,
)
Icon(
imageVector = Icons.Rounded.Warning,
contentDescription = stringRes(R.string.content_warning),
modifier = Modifier.size(30.dp).align(Alignment.TopEnd),
modifier =
Modifier
.size(30.dp)
.align(Alignment.TopEnd),
tint = MaterialTheme.colorScheme.onBackground,
)
}

View File

@ -26,17 +26,15 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import kotlinx.coroutines.CancellationException
class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex
}
class HiddenAccountsFeedFilter(
val account: Account,
) : FeedFilter<User>() {
override fun feedKey(): String = account.userProfile().pubkeyHex
override fun showHiddenKey(): Boolean {
return true
}
override fun showHiddenKey(): Boolean = true
override fun feed(): List<User> {
return account.flowHiddenUsers.value.hiddenUsers.reversed().mapNotNull {
override fun feed(): List<User> =
account.flowHiddenUsers.value.hiddenUsers.reversed().mapNotNull {
try {
LocalCache.getOrCreateUser(it)
} catch (e: Exception) {
@ -45,33 +43,26 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
null
}
}
}
}
class HiddenWordsFeedFilter(val account: Account) : FeedFilter<String>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex
}
class HiddenWordsFeedFilter(
val account: Account,
) : FeedFilter<String>() {
override fun feedKey(): String = account.userProfile().pubkeyHex
override fun showHiddenKey(): Boolean {
return true
}
override fun showHiddenKey(): Boolean = true
override fun feed(): List<String> {
return account.flowHiddenUsers.value.hiddenWords.toList()
}
override fun feed(): List<String> =
account.flowHiddenUsers.value.hiddenWords
.toList()
}
class SpammerAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex
}
class SpammerAccountsFeedFilter(
val account: Account,
) : FeedFilter<User>() {
override fun feedKey(): String = account.userProfile().pubkeyHex
override fun showHiddenKey(): Boolean {
return true
}
override fun showHiddenKey(): Boolean = true
override fun feed(): List<User> {
return (account.transientHiddenUsers).map { LocalCache.getOrCreateUser(it) }
}
override fun feed(): List<User> = account.transientHiddenUsers.value.map { LocalCache.getOrCreateUser(it) }
}

View File

@ -97,6 +97,7 @@ import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.ui.components.LoadNote
@ -758,6 +759,7 @@ class FollowListViewModel(
private val _kind3GlobalPeopleRoutes =
combineTransform(livePeopleListsFlow, liveKind3FollowsFlow) { myLivePeopleListsFlow, myLiveKind3FollowsFlow ->
checkNotInMainThread()
emit(
listOf(listOf(kind3Follow, globalFollow), myLivePeopleListsFlow, myLiveKind3FollowsFlow, listOf(muteListFollow))
.flatten()
@ -768,6 +770,7 @@ class FollowListViewModel(
private val _kind3GlobalPeople =
combineTransform(livePeopleListsFlow, liveKind3FollowsFlow) { myLivePeopleListsFlow, myLiveKind3FollowsFlow ->
checkNotInMainThread()
emit(
listOf(listOf(kind3Follow, globalFollow), myLivePeopleListsFlow, listOf(muteListFollow))
.flatten()

View File

@ -414,7 +414,7 @@ fun WatchBookmarksFollowsAndAccount(
isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note),
isLoggedUser = accountViewModel.isLoggedUser(note.author),
isSensitive = note.event?.isSensitive() ?: false,
showSensitiveContent = showSensitiveContent,
showSensitiveContent = showSensitiveContent.value,
)
launch(Dispatchers.Main) {

View File

@ -111,6 +111,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.joinAll
@ -308,22 +309,19 @@ class AccountViewModel(
noteIsHiddenFlows.get(note)
?: combineTransform(
account.flowHiddenUsers,
account.liveKind3FollowsFlow,
account.liveKind3Follows,
note.flow().metadata.stateFlow,
note.flow().reports.stateFlow,
) { hiddenUsers, followingUsers, metadata, reports ->
val isAcceptable =
withContext(Dispatchers.IO) {
isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.users)
}
emit(isAcceptable)
}.stateIn(
viewModelScope,
SharingStarted.Eagerly,
NoteComposeReportState(),
).also {
noteIsHiddenFlows.put(note, it)
}
emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.users))
}.flowOn(Dispatchers.Default)
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
NoteComposeReportState(),
).also {
noteIsHiddenFlows.put(note, it)
}
private val noteMustShowExpandButtonFlows = LruCache<Note, StateFlow<Boolean>>(300)