diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 3b21ef66d..f0d8e2e98 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -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, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 81a9f568e..c9b1ecf34 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -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 = MutableStateFlow(null), var warnAboutPostsWithReports: Boolean = true, var filterSpamFromStrangers: Boolean = true, var lastReadPerRoute: Map = mapOf(), @@ -202,7 +201,7 @@ class Account( var pendingAttestations: MutableStateFlow> = MutableStateFlow>(mapOf()), val scope: CoroutineScope = Amethyst.instance.applicationIOScope, ) { - var transientHiddenUsers: Set = setOf() + var transientHiddenUsers: MutableStateFlow> = 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 = userProfile().flow().follows.stateFlow.transformLatest { + checkNotInMainThread() emit( LiveFollowLists( it.user.cachedFollowingKeySet(), @@ -394,6 +396,7 @@ class Account( peopleListFollowsSource: Flow, ): Flow = 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 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() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt index 22d449c4c..edfe68d00 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt @@ -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> { - return subscriptions - } + fun allSubscriptions(): Map> = subscriptions - fun getSubscriptionFilters(subId: String): List { - return subscriptions[subId] ?: emptyList() - } + fun getSubscriptionFilters(subId: String): List = subscriptions[subId] ?: emptyList() abstract class Listener { /** A new message was received */ diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt index 3866f3447..0dfafb94e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt @@ -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, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt index 58fedc1d0..1a04da166 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt @@ -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() { - override fun feedKey(): String { - return account.userProfile().pubkeyHex - } +class HiddenAccountsFeedFilter( + val account: Account, +) : FeedFilter() { + override fun feedKey(): String = account.userProfile().pubkeyHex - override fun showHiddenKey(): Boolean { - return true - } + override fun showHiddenKey(): Boolean = true - override fun feed(): List { - return account.flowHiddenUsers.value.hiddenUsers.reversed().mapNotNull { + override fun feed(): List = + account.flowHiddenUsers.value.hiddenUsers.reversed().mapNotNull { try { LocalCache.getOrCreateUser(it) } catch (e: Exception) { @@ -45,33 +43,26 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter() { null } } - } } -class HiddenWordsFeedFilter(val account: Account) : FeedFilter() { - override fun feedKey(): String { - return account.userProfile().pubkeyHex - } +class HiddenWordsFeedFilter( + val account: Account, +) : FeedFilter() { + override fun feedKey(): String = account.userProfile().pubkeyHex - override fun showHiddenKey(): Boolean { - return true - } + override fun showHiddenKey(): Boolean = true - override fun feed(): List { - return account.flowHiddenUsers.value.hiddenWords.toList() - } + override fun feed(): List = + account.flowHiddenUsers.value.hiddenWords + .toList() } -class SpammerAccountsFeedFilter(val account: Account) : FeedFilter() { - override fun feedKey(): String { - return account.userProfile().pubkeyHex - } +class SpammerAccountsFeedFilter( + val account: Account, +) : FeedFilter() { + override fun feedKey(): String = account.userProfile().pubkeyHex - override fun showHiddenKey(): Boolean { - return true - } + override fun showHiddenKey(): Boolean = true - override fun feed(): List { - return (account.transientHiddenUsers).map { LocalCache.getOrCreateUser(it) } - } + override fun feed(): List = account.transientHiddenUsers.value.map { LocalCache.getOrCreateUser(it) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index f8faac59e..82aabddcc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -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() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index ffa6e8a74..374f8bc8a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -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) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 23371bf3c..f24d669cd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -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>(300)