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
@@ -328,10 +328,10 @@ object LocalPreferences {
) )
putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, account.hasDonatedInVersion) putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, account.hasDonatedInVersion)
if (account.showSensitiveContent == null) { if (account.showSensitiveContent.value == null) {
remove(PrefKeys.SHOW_SENSITIVE_CONTENT) remove(PrefKeys.SHOW_SENSITIVE_CONTENT)
} else { } else {
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!) putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent.value!!)
} }
putString( putString(
@@ -597,7 +597,7 @@ object LocalPreferences {
backupContactList = latestContactList, backupContactList = latestContactList,
proxy = proxy, proxy = proxy,
proxyPort = proxyPort, proxyPort = proxyPort,
showSensitiveContent = showSensitiveContent, showSensitiveContent = MutableStateFlow(showSensitiveContent),
warnAboutPostsWithReports = warnAboutReports, warnAboutPostsWithReports = warnAboutReports,
filterSpamFromStrangers = filterSpam, filterSpamFromStrangers = filterSpam,
lastReadPerRoute = lastReadPerRoute, lastReadPerRoute = lastReadPerRoute,
@@ -26,7 +26,6 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.core.os.ConfigurationCompat import androidx.core.os.ConfigurationCompat
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData import androidx.lifecycle.liveData
import androidx.lifecycle.switchMap import androidx.lifecycle.switchMap
@@ -194,7 +193,7 @@ class Account(
var backupContactList: ContactListEvent? = null, var backupContactList: ContactListEvent? = null,
var proxy: Proxy? = null, var proxy: Proxy? = null,
var proxyPort: Int = 9050, var proxyPort: Int = 9050,
var showSensitiveContent: Boolean? = null, var showSensitiveContent: MutableStateFlow<Boolean?> = MutableStateFlow(null),
var warnAboutPostsWithReports: Boolean = true, var warnAboutPostsWithReports: Boolean = true,
var filterSpamFromStrangers: Boolean = true, var filterSpamFromStrangers: Boolean = true,
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>(), 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()), var pendingAttestations: MutableStateFlow<Map<HexKey, String>> = MutableStateFlow<Map<HexKey, String>>(mapOf()),
val scope: CoroutineScope = Amethyst.instance.applicationIOScope, val scope: CoroutineScope = Amethyst.instance.applicationIOScope,
) { ) {
var transientHiddenUsers: Set<String> = setOf() var transientHiddenUsers: MutableStateFlow<Set<String>> = MutableStateFlow(setOf())
data class PaymentRequest( data class PaymentRequest(
val relayUrl: String, val relayUrl: String,
@@ -238,6 +237,8 @@ class Account(
getPrivateOutboxRelayListFlow(), getPrivateOutboxRelayListFlow(),
userProfile().flow().relays.stateFlow, userProfile().flow().relays.stateFlow,
) { nip65RelayList, dmRelayList, searchRelayList, privateOutBox, userProfile -> ) { nip65RelayList, dmRelayList, searchRelayList, privateOutBox, userProfile ->
checkNotInMainThread()
val baseRelaySet = activeRelays() ?: convertLocalRelays() val baseRelaySet = activeRelays() ?: convertLocalRelays()
val newDMRelaySet = (dmRelayList.note.event as? ChatMessageRelayListEvent)?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet() 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 val searchRelaySet = (searchRelayList.note.event as? SearchRelayListEvent)?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: Constants.defaultSearchRelaySet
@@ -358,6 +359,7 @@ class Account(
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
val liveKind3FollowsFlow: Flow<LiveFollowLists> = val liveKind3FollowsFlow: Flow<LiveFollowLists> =
userProfile().flow().follows.stateFlow.transformLatest { userProfile().flow().follows.stateFlow.transformLatest {
checkNotInMainThread()
emit( emit(
LiveFollowLists( LiveFollowLists(
it.user.cachedFollowingKeySet(), it.user.cachedFollowingKeySet(),
@@ -394,6 +396,7 @@ class Account(
peopleListFollowsSource: Flow<ListNameNotePair>, peopleListFollowsSource: Flow<ListNameNotePair>,
): Flow<LiveFollowLists?> = ): Flow<LiveFollowLists?> =
combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows -> combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows ->
checkNotInMainThread()
if (peopleListFollows.listName == GLOBAL_FOLLOWS) { if (peopleListFollows.listName == GLOBAL_FOLLOWS) {
emit(null) emit(null)
} else if (peopleListFollows.listName == KIND3_FOLLOWS) { } else if (peopleListFollows.listName == KIND3_FOLLOWS) {
@@ -500,10 +503,11 @@ class Account(
val flowHiddenUsers: StateFlow<LiveHiddenUsers> by lazy { val flowHiddenUsers: StateFlow<LiveHiddenUsers> by lazy {
combineTransform( combineTransform(
live.asFlow(), transientHiddenUsers,
showSensitiveContent,
getBlockListNote().flow().metadata.stateFlow, getBlockListNote().flow().metadata.stateFlow,
getMuteListNote().flow().metadata.stateFlow, getMuteListNote().flow().metadata.stateFlow,
) { localLive, blockList, muteList -> ) { transientHiddenUsers, showSensitiveContent, blockList, muteList ->
checkNotInMainThread() checkNotInMainThread()
val resultBlockList = val resultBlockList =
@@ -532,8 +536,8 @@ class Account(
LiveHiddenUsers( LiveHiddenUsers(
hiddenUsers = (resultBlockList.users + resultMuteList.users), hiddenUsers = (resultBlockList.users + resultMuteList.users),
hiddenWords = hiddenWords, hiddenWords = hiddenWords,
spammers = localLive.account.transientHiddenUsers, spammers = transientHiddenUsers,
showSensitiveContent = localLive.account.showSensitiveContent, showSensitiveContent = showSensitiveContent,
), ),
) )
}.stateIn( }.stateIn(
@@ -542,8 +546,8 @@ class Account(
LiveHiddenUsers( LiveHiddenUsers(
hiddenUsers = setOf(), hiddenUsers = setOf(),
hiddenWords = setOf(), hiddenWords = setOf(),
spammers = transientHiddenUsers, spammers = transientHiddenUsers.value,
showSensitiveContent = showSensitiveContent, showSensitiveContent = showSensitiveContent.value,
), ),
) )
} }
@@ -596,7 +600,9 @@ class Account(
filterSpamFromStrangers = filterSpam filterSpamFromStrangers = filterSpam
LocalCache.antiSpam.active = filterSpamFromStrangers LocalCache.antiSpam.active = filterSpamFromStrangers
if (!filterSpamFromStrangers) { if (!filterSpamFromStrangers) {
transientHiddenUsers = setOf() transientHiddenUsers.update {
emptySet()
}
} }
live.invalidateData() live.invalidateData()
saveable.invalidateData() saveable.invalidateData()
@@ -2377,7 +2383,9 @@ class Account(
} }
} }
transientHiddenUsers = (transientHiddenUsers - pubkeyHex) transientHiddenUsers.update {
it - pubkeyHex
}
live.invalidateData() live.invalidateData()
saveable.invalidateData() saveable.invalidateData()
} }
@@ -2889,7 +2897,9 @@ class Account(
} }
fun updateShowSensitiveContent(show: Boolean?) { fun updateShowSensitiveContent(show: Boolean?) {
showSensitiveContent = show showSensitiveContent.update {
show
}
saveable.invalidateData() saveable.invalidateData()
live.invalidateData() live.invalidateData()
} }
@@ -2928,10 +2938,12 @@ class Account(
// imports transient blocks due to spam. // imports transient blocks due to spam.
LocalCache.antiSpam.liveSpam.observeForever { LocalCache.antiSpam.liveSpam.observeForever {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
it.cache.spamMessages.snapshot().values.forEach { it.cache.spamMessages.snapshot().values.forEach { spammer ->
if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) { if (spammer.pubkeyHex !in transientHiddenUsers.value && spammer.duplicatedMessages.size >= 5) {
if (it.pubkeyHex != userProfile().pubkeyHex && it.pubkeyHex !in followingKeySet()) { if (spammer.pubkeyHex != userProfile().pubkeyHex && spammer.pubkeyHex !in followingKeySet()) {
transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex) transientHiddenUsers.update {
it + spammer.pubkeyHex
}
live.invalidateData() live.invalidateData()
} }
} }
@@ -179,9 +179,7 @@ object Client : RelayPool.Listener {
subscriptions = subscriptions.minus(subscriptionId) subscriptions = subscriptions.minus(subscriptionId)
} }
fun isActive(subscriptionId: String): Boolean { fun isActive(subscriptionId: String): Boolean = subscriptions.contains(subscriptionId)
return subscriptions.contains(subscriptionId)
}
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
override fun onEvent( override fun onEvent(
@@ -205,9 +203,9 @@ object Client : RelayPool.Listener {
) { ) {
// Releases the Web thread for the new payload. // Releases the Web thread for the new payload.
// May need to add a processing queue if processing new events become too costly. // May need to add a processing queue if processing new events become too costly.
GlobalScope.launch(Dispatchers.Default) { // GlobalScope.launch(Dispatchers.Default) {
listeners.forEach { it.onRelayStateChange(type, relay, channel) } listeners.forEach { it.onRelayStateChange(type, relay, channel) }
} // }
} }
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
@@ -249,21 +247,15 @@ object Client : RelayPool.Listener {
listeners = listeners.plus(listener) listeners = listeners.plus(listener)
} }
fun isSubscribed(listener: Listener): Boolean { fun isSubscribed(listener: Listener): Boolean = listeners.contains(listener)
return listeners.contains(listener)
}
fun unsubscribe(listener: Listener) { fun unsubscribe(listener: Listener) {
listeners = listeners.minus(listener) listeners = listeners.minus(listener)
} }
fun allSubscriptions(): Map<String, List<TypedFilter>> { fun allSubscriptions(): Map<String, List<TypedFilter>> = subscriptions
return subscriptions
}
fun getSubscriptionFilters(subId: String): List<TypedFilter> { fun getSubscriptionFilters(subId: String): List<TypedFilter> = subscriptions[subId] ?: emptyList()
return subscriptions[subId] ?: emptyList()
}
abstract class Listener { abstract class Listener {
/** A new message was received */ /** A new message was received */
@@ -39,7 +39,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
@@ -97,10 +97,9 @@ fun SensitivityWarning(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState = accountViewModel.account.showSensitiveContent.collectAsStateWithLifecycle()
var showContentWarningNote by var showContentWarningNote by remember(accountState) { mutableStateOf(accountState.value != true) }
remember(accountState) { mutableStateOf(accountState?.account?.showSensitiveContent != true) }
CrossfadeIfEnabled(targetState = showContentWarningNote, accountViewModel = accountViewModel) { CrossfadeIfEnabled(targetState = showContentWarningNote, accountViewModel = accountViewModel) {
if (it) { if (it) {
@@ -118,18 +117,26 @@ fun ContentWarningNote(onDismiss: () -> Unit) {
Column(modifier = Modifier.padding(start = 10.dp)) { Column(modifier = Modifier.padding(start = 10.dp)) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Box( Box(
Modifier.height(80.dp).width(90.dp), Modifier
.height(80.dp)
.width(90.dp),
) { ) {
Icon( Icon(
imageVector = Icons.Default.Visibility, imageVector = Icons.Default.Visibility,
contentDescription = stringRes(R.string.content_warning), 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, tint = MaterialTheme.colorScheme.onBackground,
) )
Icon( Icon(
imageVector = Icons.Rounded.Warning, imageVector = Icons.Rounded.Warning,
contentDescription = stringRes(R.string.content_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, tint = MaterialTheme.colorScheme.onBackground,
) )
} }
@@ -26,17 +26,15 @@ import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() { class HiddenAccountsFeedFilter(
override fun feedKey(): String { val account: Account,
return account.userProfile().pubkeyHex ) : FeedFilter<User>() {
} override fun feedKey(): String = account.userProfile().pubkeyHex
override fun showHiddenKey(): Boolean { override fun showHiddenKey(): Boolean = true
return true
}
override fun feed(): List<User> { override fun feed(): List<User> =
return account.flowHiddenUsers.value.hiddenUsers.reversed().mapNotNull { account.flowHiddenUsers.value.hiddenUsers.reversed().mapNotNull {
try { try {
LocalCache.getOrCreateUser(it) LocalCache.getOrCreateUser(it)
} catch (e: Exception) { } catch (e: Exception) {
@@ -45,33 +43,26 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
null null
} }
} }
}
} }
class HiddenWordsFeedFilter(val account: Account) : FeedFilter<String>() { class HiddenWordsFeedFilter(
override fun feedKey(): String { val account: Account,
return account.userProfile().pubkeyHex ) : FeedFilter<String>() {
} override fun feedKey(): String = account.userProfile().pubkeyHex
override fun showHiddenKey(): Boolean { override fun showHiddenKey(): Boolean = true
return true
}
override fun feed(): List<String> { override fun feed(): List<String> =
return account.flowHiddenUsers.value.hiddenWords.toList() account.flowHiddenUsers.value.hiddenWords
} .toList()
} }
class SpammerAccountsFeedFilter(val account: Account) : FeedFilter<User>() { class SpammerAccountsFeedFilter(
override fun feedKey(): String { val account: Account,
return account.userProfile().pubkeyHex ) : FeedFilter<User>() {
} override fun feedKey(): String = account.userProfile().pubkeyHex
override fun showHiddenKey(): Boolean { override fun showHiddenKey(): Boolean = true
return true
}
override fun feed(): List<User> { override fun feed(): List<User> = account.transientHiddenUsers.value.map { LocalCache.getOrCreateUser(it) }
return (account.transientHiddenUsers).map { LocalCache.getOrCreateUser(it) }
}
} }
@@ -97,6 +97,7 @@ import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrThreadDataSource
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.NostrVideoDataSource 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.Client
import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.LoadNote
@@ -758,6 +759,7 @@ class FollowListViewModel(
private val _kind3GlobalPeopleRoutes = private val _kind3GlobalPeopleRoutes =
combineTransform(livePeopleListsFlow, liveKind3FollowsFlow) { myLivePeopleListsFlow, myLiveKind3FollowsFlow -> combineTransform(livePeopleListsFlow, liveKind3FollowsFlow) { myLivePeopleListsFlow, myLiveKind3FollowsFlow ->
checkNotInMainThread()
emit( emit(
listOf(listOf(kind3Follow, globalFollow), myLivePeopleListsFlow, myLiveKind3FollowsFlow, listOf(muteListFollow)) listOf(listOf(kind3Follow, globalFollow), myLivePeopleListsFlow, myLiveKind3FollowsFlow, listOf(muteListFollow))
.flatten() .flatten()
@@ -768,6 +770,7 @@ class FollowListViewModel(
private val _kind3GlobalPeople = private val _kind3GlobalPeople =
combineTransform(livePeopleListsFlow, liveKind3FollowsFlow) { myLivePeopleListsFlow, myLiveKind3FollowsFlow -> combineTransform(livePeopleListsFlow, liveKind3FollowsFlow) { myLivePeopleListsFlow, myLiveKind3FollowsFlow ->
checkNotInMainThread()
emit( emit(
listOf(listOf(kind3Follow, globalFollow), myLivePeopleListsFlow, listOf(muteListFollow)) listOf(listOf(kind3Follow, globalFollow), myLivePeopleListsFlow, listOf(muteListFollow))
.flatten() .flatten()
@@ -414,7 +414,7 @@ fun WatchBookmarksFollowsAndAccount(
isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note), isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note),
isLoggedUser = accountViewModel.isLoggedUser(note.author), isLoggedUser = accountViewModel.isLoggedUser(note.author),
isSensitive = note.event?.isSensitive() ?: false, isSensitive = note.event?.isSensitive() ?: false,
showSensitiveContent = showSensitiveContent, showSensitiveContent = showSensitiveContent.value,
) )
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
@@ -111,6 +111,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.joinAll import kotlinx.coroutines.joinAll
@@ -308,22 +309,19 @@ class AccountViewModel(
noteIsHiddenFlows.get(note) noteIsHiddenFlows.get(note)
?: combineTransform( ?: combineTransform(
account.flowHiddenUsers, account.flowHiddenUsers,
account.liveKind3FollowsFlow, account.liveKind3Follows,
note.flow().metadata.stateFlow, note.flow().metadata.stateFlow,
note.flow().reports.stateFlow, note.flow().reports.stateFlow,
) { hiddenUsers, followingUsers, metadata, reports -> ) { hiddenUsers, followingUsers, metadata, reports ->
val isAcceptable = emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.users))
withContext(Dispatchers.IO) { }.flowOn(Dispatchers.Default)
isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.users) .stateIn(
} viewModelScope,
emit(isAcceptable) SharingStarted.Eagerly,
}.stateIn( NoteComposeReportState(),
viewModelScope, ).also {
SharingStarted.Eagerly, noteIsHiddenFlows.put(note, it)
NoteComposeReportState(), }
).also {
noteIsHiddenFlows.put(note, it)
}
private val noteMustShowExpandButtonFlows = LruCache<Note, StateFlow<Boolean>>(300) private val noteMustShowExpandButtonFlows = LruCache<Note, StateFlow<Boolean>>(300)