refactor: delete the main-thread checker

checkNotInMainThread was a debug-only Android assertion (Looper check behind BuildConfig.DEBUG).
Once the cache, feeds and view models moved to commons it could only fire through the
LocalCacheHost.assertNotMainThread hook, which is a no-op on every other front end, so it no
longer guarded the shared code it was sprinkled through. The checker, its call sites and the
host hook are removed; the two comments that explained a Dispatchers.IO switch by the assertion
now give the real reason (compression blocks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
Claude
2026-09-29 03:32:05 +00:00
parent 472dfd4670
commit 7913ce156e
25 changed files with 8 additions and 141 deletions
@@ -47,7 +47,6 @@ Under `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/`:
- **`commons/src/commonMain/.../threading/Threading.kt`** — shared dispatchers and `CoroutineScope` helpers for commonMain code.
- **`amethyst/src/main/java/.../service/CoroutinesExt.kt`** — Android-only helpers: `launchIO(block)`, `launchMain(block)` built on top of `Dispatchers.IO` / `Dispatchers.Main`. Use these in ViewModels and services to stop re-spelling the dispatcher every time.
- **`amethyst/src/main/java/.../service/MainThreadChecker.kt`** — debug assertion helper for catching main-thread misuse during dev.
## Quartz iterables & JSON
@@ -24,7 +24,6 @@ import com.vitorpamplona.amethyst.commons.model.cache.FileSystemNip95BlobStore
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.cache.LocalCacheHost
import com.vitorpamplona.amethyst.commons.model.cache.Nip95BlobStore
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -63,6 +62,4 @@ class AmethystLocalCacheHost(
// The zap path's LNURL cache is a quartz-side singleton the outbound-zap resolver fills.
override fun lnurlEndpoint(lnurlpUrl: String): LnurlEndpointInfo? = LnurlEndpointCache.get(lnurlpUrl)
override fun assertNotMainThread() = checkNotInMainThread()
}
@@ -64,7 +64,6 @@ import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPolicy
import com.vitorpamplona.amethyst.model.backups.BackupConflictStorage
import com.vitorpamplona.amethyst.model.nip60Cashu.CashuPreferences
import com.vitorpamplona.amethyst.model.preferences.UiSharedPreferences
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
@@ -674,17 +673,14 @@ object LocalPreferences {
}
}
private fun encryptedPreferences(npub: String? = null): SharedPreferences {
checkNotInMainThread()
return if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) {
private fun encryptedPreferences(npub: String? = null): SharedPreferences =
if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) {
val preferenceFile =
if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub"
Amethyst.instance.appContext.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
} else {
Amethyst.instance.encryptedStorage(npub)
}
}
/**
* Clears the preferences for a given npub, deletes the preferences xml file, and switches the
@@ -1,36 +0,0 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service
import android.os.Looper
import com.vitorpamplona.amethyst.BuildConfig
fun checkNotInMainThread() {
if (BuildConfig.DEBUG && isMainThread()) {
throw OnMainThreadException("It should not be in the MainThread")
}
}
fun isMainThread() = Looper.myLooper() == Looper.getMainLooper()
class OnMainThreadException(
str: String,
) : RuntimeException(str)
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.service.cashu
import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.nip60Cashu.token.CashuToken
import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenB64Parser
import kotlinx.collections.immutable.ImmutableList
@@ -34,8 +33,6 @@ import kotlinx.collections.immutable.toImmutableList
*/
class CashuParser {
fun parse(cashuToken: String): GenericLoadable<ImmutableList<CashuToken>> {
checkNotInMainThread()
val parsed =
CashuTokenB64Parser.parse(cashuToken)
?: return GenericLoadable.Error("Could not parse this cashu token")
@@ -26,7 +26,6 @@ import android.net.Uri
import androidx.core.net.toUri
import androidx.media3.common.MimeTypes
import com.davotoula.lightcompressor.video.GifToMp4Converter
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.components.util.MediaCompressorFileUtils
import com.vitorpamplona.quartz.utils.Log
import id.zelory.compressor.Compressor
@@ -50,8 +49,6 @@ class MediaCompressor {
useH265: Boolean = false,
convertGifToMp4: Boolean = false,
): MediaCompressorResult {
checkNotInMainThread()
// Convert GIF to MP4 if requested. The GIF converter already produces a well-compressed
// H.264 MP4 so no additional video compression step is needed.
if (convertGifToMp4 && contentType?.contains("gif", ignoreCase = true) == true) {
@@ -31,7 +31,6 @@ import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_to_server_w
import com.vitorpamplona.amethyst.commons.service.HttpStatusMessages
import com.vitorpamplona.amethyst.commons.service.upload.BlossomPaymentException
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
import com.vitorpamplona.amethyst.service.uploads.PreviewMetadataCalculator
import com.vitorpamplona.amethyst.service.uploads.extensionFromMimeType
@@ -86,8 +85,6 @@ class BlossomUploader {
useMediaEndpoint: Boolean = false,
onProgress: ((bytesWritten: Long, totalBytes: Long) -> Unit)? = null,
): MediaUploadResult {
checkNotInMainThread()
val contentResolver = context.contentResolver
val myContentType = contentType ?: contentResolver.getType(uri)
val fileName = context.getFileName(uri)
@@ -141,8 +138,6 @@ class BlossomUploader {
useMediaEndpoint: Boolean = false,
onProgress: ((bytesWritten: Long, totalBytes: Long) -> Unit)? = null,
): MediaUploadResult {
checkNotInMainThread()
val fileName = baseFileName ?: RandomInstance.randomChars(16)
val extension =
contentType?.let {
@@ -32,7 +32,6 @@ import com.vitorpamplona.amethyst.commons.resources.failed_to_delete_with_messag
import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_to_server_with_message
import com.vitorpamplona.amethyst.commons.service.HttpStatusMessages
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.uploads.AVIF_EXTENSION
import com.vitorpamplona.amethyst.service.uploads.AVIF_MIME
import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult
@@ -105,8 +104,6 @@ class Nip96Uploader {
httpAuth: suspend (String, String, ByteArray?) -> HTTPAuthorizationEvent?,
context: Context,
): MediaUploadResult {
checkNotInMainThread()
val contentResolver = context.contentResolver
val myContentType = contentType ?: contentResolver.getType(uri)
val length = size ?: contentResolver.querySize(uri) ?: fileSize(uri) ?: 0
@@ -145,8 +142,6 @@ class Nip96Uploader {
httpAuth: suspend (String, String, ByteArray?) -> HTTPAuthorizationEvent?,
context: Context,
): MediaUploadResult {
checkNotInMainThread()
val fileName = RandomInstance.randomChars(16)
val extension =
contentType?.let {
@@ -32,7 +32,6 @@ import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.service.BundledUpdate
import com.vitorpamplona.amethyst.commons.util.equalImmutableLists
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.utils.Log
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@@ -56,8 +55,6 @@ open class UserFeedViewModel(
override val isRefreshing: MutableState<Boolean> = mutableStateOf(false)
private fun refreshSuspended() {
checkNotInMainThread()
try {
isRefreshing.value = true
@@ -1039,13 +1039,10 @@ private suspend fun sendAttachment(
try {
// The media-quality slider.
//
// Off the main thread, like the strip and the read below it.
// Off the main thread, like the strip and the read below it:
// `sendAttachment` is called from the composition's scope, so it
// inherits Main, and `MediaCompressor.compress` opens with
// `checkNotInMainThread()` — so every image attachment threw
// `OnMainThreadException` before it ever reached the uploader. The
// voice path never hit it because it posts its recording already
// encoded and skips compression entirely.
// inherits Main, and compression blocks. The voice path posts its
// recording already encoded and skips compression entirely.
val compressed =
withContext(Dispatchers.IO) {
item.orchestrator.compressIfNeeded(
@@ -54,10 +54,8 @@ class ConcordImageUploader(
/**
* Compresses, strips, AES-256-GCM-encrypts and uploads the picked [uri], returning its pointer.
*
* Runs on [Dispatchers.IO]: the compression/upload pipeline asserts it is off the main thread
* ([MediaCompressor] calls `checkNotInMainThread`), and callers launch this from a Compose
* `rememberCoroutineScope()`, which is Main-dispatched — so without this switch the first step
* throws before any bytes leave the device.
* Runs on [Dispatchers.IO]: compression, encryption and the upload all block, and callers launch
* this from a Compose `rememberCoroutineScope()`, which is Main-dispatched.
*/
suspend fun uploadEncrypted(
uri: Uri,
@@ -31,7 +31,6 @@ import com.vitorpamplona.amethyst.commons.feeds.StringFeedState
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.service.BundledUpdate
import com.vitorpamplona.amethyst.commons.util.equalImmutableLists
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.utils.Log
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@@ -55,8 +54,6 @@ open class StringFeedViewModel(
}
private fun refreshSuspended() {
checkNotInMainThread()
try {
isRefreshing.value = true
@@ -30,7 +30,6 @@ import com.google.mlkit.nl.translate.TranslateLanguage
import com.google.mlkit.nl.translate.Translation
import com.google.mlkit.nl.translate.Translator
import com.google.mlkit.nl.translate.TranslatorOptions
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -91,7 +90,6 @@ object LanguageTranslatorService {
source: String,
target: String,
): Task<ResultOrError> {
checkNotInMainThread()
val sourceLangCode = TranslateLanguage.fromLanguageTag(source)
val targetLangCode = TranslateLanguage.fromLanguageTag(target)
@@ -110,8 +108,6 @@ object LanguageTranslatorService {
val translator = translators[options]
return translator.downloadModelIfNeeded().onSuccessTask(executorService) {
checkNotInMainThread()
val dict = TranslationDictionary.build(text)
val encoded = TranslationDictionary.encode(text, dict)
@@ -119,7 +119,7 @@ Each leaving edge is a seam to cut before the group can move.
| `Amethyst.kt` | `keyCache` (Account), `encryptedStorage` (Geohash), `appContext` (Nip46 bridge), `notificationDispatcher` (EventProcessor) | constructor parameters / ports; the notification dispatcher gets an interface. **Done 2026-09-28**: `keyCache` → `Account.encryptionKeyCache`; the notifications → `MarmotGroupNotifier`, which `NotificationDispatcher` implements; `encryptedStorage` and `appContext` left with the Geohash and Nip46 cuts. |
| `LocalPreferences.kt` | `saveToEncryptedStorage(accountSettings)` on settings change | **Done 2026-09-28** as a constructor lambda, `Account.saveSettings: suspend (AccountSettings) -> Unit`, rather than a named port. |
| `DebugUtils.kt` | `logTime` (2 sites) | **Done 2026-09-28.** `commons/util` already had an identical `logTime`; its Android `isDebug` was hard-coded `false`, so it now reads `AndroidDebugFlag.enabled`, which `Amethyst.onCreate` sets. The app copy is gone. |
| `service/MainThreadChecker.kt` | `checkNotInMainThread` (HiddenUsersState) | **Done 2026-09-28**: `LocalCache.appHost.assertNotMainThread()`, which the app host delegates to the same check. |
| `service/MainThreadChecker.kt` | `checkNotInMainThread` (HiddenUsersState) | **Done 2026-09-28**: `LocalCache.appHost.assertNotMainThread()`. **Deleted 2026-09-29**: a debug-only Android assertion that shared code could no longer rely on; the checker, its call sites and the host hook are gone. |
| `service/location/LocationState.kt` | the `LocationResult` type in `geolocationFlow` and the around-me feed | **Done 2026-09-28**: `commons/model/location/LocationResult.kt`; 25 files repointed. |
| `service/uploads/FileHeader.kt` | the `FileHeader` data type in three send methods | **Done 2026-09-28**: `commons/service/upload/FileHeader.kt` (with `BlurhashWrapper`/`ThumbhashWrapper` moved from commonsUI, same package); `prepare` stays in the app as `FileHeader.Companion` extensions. |
| `service/relayClient/…/BuzzMembershipEoseManager.kt` | the `MembershipNotificationKinds` constant | **Done 2026-09-28**: `commons/model/buzz/BuzzMembershipKinds.kt`. |
@@ -25,7 +25,6 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.service.BundledInsert
import com.vitorpamplona.amethyst.commons.service.BundledUpdate
import com.vitorpamplona.amethyst.commons.util.equalImmutableLists
@@ -73,8 +72,6 @@ class ChannelFeedContentState(
}
fun refreshSuspended() {
LocalCache.appHost.assertNotMainThread()
isRefreshing.value = true
try {
lastFeedKey = localFilter.feedKey()
@@ -162,8 +162,6 @@ class CachePruner(
}
fun pruneOldMessages() {
cache.appHost.assertNotMainThread()
cache.ephemeralChannels.forEach { _, channel ->
pruneOldMessagesChannel(channel)
}
@@ -310,8 +308,6 @@ class CachePruner(
}
fun pruneRepliesAndReactions(accounts: Set<HexKey>) {
cache.appHost.assertNotMainThread()
val toBeRemoved =
cache.notes.filter { _, note ->
(
@@ -448,8 +444,6 @@ class CachePruner(
}
fun pruneExpiredEvents() {
cache.appHost.assertNotMainThread()
val now = TimeUtils.now()
val versionsToBeRemoved = cache.notes.filter { _, it -> it.event?.isExpirationBefore(now) == true }
val addressesToBeRemoved = cache.addressables.filter { _, it -> it.event?.isExpirationBefore(now) == true }
@@ -481,8 +475,6 @@ class CachePruner(
* authors the reader never muted.
*/
fun pruneHiddenEvents(hidden: LiveHiddenUsers) {
cache.appHost.assertNotMainThread()
val childrenToBeRemoved = mutableListOf<Note>()
val toBeRemoved =
@@ -69,8 +69,6 @@ class CacheSearch(
): List<User> {
if (username.isBlank()) return emptyList()
cache.appHost.assertNotMainThread()
val key = decodePublicKeyAsHexOrNull(username)
if (key != null) {
@@ -164,8 +162,6 @@ class CacheSearch(
filters: List<Filter>,
hidden: LiveHiddenUsers,
): List<Note> {
cache.appHost.assertNotMainThread()
if (filters.isEmpty()) return emptyList()
// Distinct across filters: a union of arms (a hashtag asks #t, #l and the comment tags)
@@ -201,8 +197,6 @@ class CacheSearch(
text: String,
hidden: LiveHiddenUsers,
): List<Note> {
cache.appHost.assertNotMainThread()
if (text.isBlank()) return emptyList()
val key = decodeEventIdAsHexOrNull(text)
@@ -3357,8 +3357,6 @@ open class EventCache :
var verifyMeter: ((elapsedNanos: Long, valid: Boolean) -> Unit)? = null
fun justVerify(event: Event): Boolean {
appHost.assertNotMainThread()
val meter = verifyMeter
if (meter == null) return justVerifyInner(event)
@@ -98,12 +98,6 @@ interface LocalCacheHost {
*/
fun lnurlEndpoint(lnurlpUrl: String): LnurlEndpointInfo? = null
/**
* Throws if called on the platform's main thread. Signature verification and the cache
* sweeps are far too slow to run there. A no-op by default and on release builds.
*/
fun assertNotMainThread() {}
/** The neutral host: an own IO scope and nothing else. */
companion object Default : LocalCacheHost {
override val scope: CoroutineScope by lazy { CoroutineScope(Dispatchers.IO + SupervisorJob()) }
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.commons.model.nip51Lists
import com.vitorpamplona.amethyst.commons.model.AccountSettings
import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.EventTag
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.HashtagTag
@@ -84,7 +83,6 @@ class HiddenUsersState(
settings.syncedSettings.security.showSensitiveContent,
settings.syncedSettings.security.maxHashtagLimit,
) { blockList, muteList, transientHiddenUsers, showSensitiveContent, maxHashtagLimit ->
LocalCache.appHost.assertNotMainThread()
emit(assembleLiveHiddenUsers(blockList, muteList, transientHiddenUsers, showSensitiveContent, maxHashtagLimit))
}.onStart {
emit(
@@ -123,8 +123,6 @@ class CardFeedContentState(
private fun refreshSuspended() = refreshLock.withLock { refreshLocked() }
private fun refreshLocked() {
LocalCache.appHost.assertNotMainThread()
try {
isRefreshing.value = true
@@ -170,8 +168,6 @@ class CardFeedContentState(
}
private fun convertToCard(notes: Collection<Note>): List<Card> {
LocalCache.appHost.assertNotMainThread()
val reactionsPerEvent = mutableMapOf<Note, MutableList<Note>>()
notes
.filter { it.event is ReactionEvent }
@@ -544,8 +540,6 @@ class CardFeedContentState(
}
fun updateFeedWith(newNotes: Set<Note>) {
LocalCache.appHost.assertNotMainThread()
if (localFilter is AdditiveFeedFilter && _feedContent.value is CardFeedState.Loaded) {
invalidateInsertData(newNotes)
} else {
@@ -24,7 +24,6 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets.InterestSet
import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter
@@ -295,7 +294,6 @@ class TopNavFilterState(
livePeopleListsFlow,
liveInterestFlows,
) { peopleLists, interests ->
LocalCache.appHost.assertNotMainThread()
emit(
listOf(
listOf(allFollows, userFollows, kind3Follows, aroundMe, teleport, globalFollow),
@@ -308,7 +306,6 @@ class TopNavFilterState(
private val _badgeRoutes =
livePeopleListsFlow.transform { peopleLists ->
LocalCache.appHost.assertNotMainThread()
emit(
listOf(
listOf(allFollows, userFollows, kind3Follows, globalFollow, mineFollow),
@@ -320,7 +317,6 @@ class TopNavFilterState(
private val _communityRoutes =
livePeopleListsFlow.transform { peopleLists ->
LocalCache.appHost.assertNotMainThread()
emit(
listOf(
listOf(allFollows, userFollows, kind3Follows, globalFollow, mineFollow),
@@ -335,7 +331,6 @@ class TopNavFilterState(
livePeopleListsFlow,
liveInterestFlows,
) { peopleLists, interests ->
LocalCache.appHost.assertNotMainThread()
emit(
listOf(
// Same content-style catalog as kind3GlobalPeopleRoutes, plus "Mine" so the
@@ -353,7 +348,6 @@ class TopNavFilterState(
livePeopleListsFlow,
liveInterestFlows,
) { peopleLists, interests ->
LocalCache.appHost.assertNotMainThread()
emit(
listOf(
// Git repository announcements can be narrowed by author, hashtag and geohash,
@@ -371,7 +365,6 @@ class TopNavFilterState(
livePeopleListsFlow,
liveInterestFlows,
) { peopleLists, interests ->
LocalCache.appHost.assertNotMainThread()
emit(
listOf(
// Workout records can be narrowed by author, hashtag and geohash, so this
@@ -389,7 +382,6 @@ class TopNavFilterState(
livePeopleListsFlow,
liveInterestFlows,
) { peopleLists, interests ->
LocalCache.appHost.assertNotMainThread()
emit(
listOf(
// Highlights can be narrowed by author, hashtag and geohash, so this mirrors
@@ -408,7 +400,6 @@ class TopNavFilterState(
liveInterestFlows,
account.relayGroupList.liveRelayGroupServers,
) { peopleLists, interests, joinedServers ->
LocalCache.appHost.assertNotMainThread()
// A relay chip per host relay of every group the user joined (kind-10009), so they can
// browse the OTHER groups on those relays without first having to Favorite them. Deduped
// against relays that are already chips because they were favorited (kind-10012, already
@@ -442,7 +433,6 @@ class TopNavFilterState(
livePeopleListsFlow,
liveInterestFlows,
) { peopleLists, interests ->
LocalCache.appHost.assertNotMainThread()
emit(
listOf(
// Same content-style catalog as kind3GlobalPeopleRoutes, plus "Mine" so the
@@ -457,7 +447,6 @@ class TopNavFilterState(
private val _kind3GlobalPeople =
livePeopleListsFlow.transform { peopleLists ->
LocalCache.appHost.assertNotMainThread()
emit(
listOf(
listOf(allFollows, userFollows, kind3Follows, aroundMe, teleport, globalFollow),
@@ -473,7 +462,6 @@ class TopNavFilterState(
// and AroundMe — none of which can match a tag-less manifest — are deliberately left out.
private val _authorOnlyRoutes =
livePeopleListsFlow.transform { peopleLists ->
LocalCache.appHost.assertNotMainThread()
emit(
listOf(
listOf(allFollows, userFollows, kind3Follows, globalFollow, mineFollow),
@@ -485,7 +473,6 @@ class TopNavFilterState(
private val _notificationLists =
livePeopleListsFlow.transform { peopleLists ->
LocalCache.appHost.assertNotMainThread()
emit(
listOf(
listOf(allFollows, userFollows, kind3Follows, aroundMe, teleport, selectedFollow, globalFollow),
@@ -89,8 +89,6 @@ class NotificationSummaryState(
fun today(): String = LocalClock.today().ymd()
suspend fun initializeSuspend() {
LocalCache.appHost.assertNotMainThread()
val days = LocalClock.epochDayCounter()
val currentUser = user.pubkeyHex
@@ -182,8 +180,6 @@ class NotificationSummaryState(
}
suspend fun addToStatsSuspend(newBlockNotes: Set<Set<Note>>) {
LocalCache.appHost.assertNotMainThread()
val days = LocalClock.epochDayCounter()
val currentUser = user.pubkeyHex
@@ -286,8 +282,6 @@ class NotificationSummaryState(
}
private suspend fun refreshChartModel() {
LocalCache.appHost.assertNotMainThread()
val today = LocalClock.today()
val dataAxisLabelIndexes = listOf(-6, -5, -4, -3, -2, -1, 0)
@@ -373,8 +373,6 @@ class AccountFeedContentStates(
}
fun updateFeedsWith(newNotes: Set<Note>) {
LocalCache.appHost.assertNotMainThread()
homeLive.updateFeedWith(newNotes)
homeNewThreads.updateFeedWith(newNotes)
homeReplies.updateFeedWith(newNotes)
@@ -447,8 +445,6 @@ class AccountFeedContentStates(
}
fun deleteNotes(newNotes: Set<Note>) {
LocalCache.appHost.assertNotMainThread()
homeLive.deleteFromFeed(newNotes)
homeNewThreads.deleteFromFeed(newNotes)
homeReplies.deleteFromFeed(newNotes)
@@ -694,8 +694,6 @@ class AccountViewModel(
accountChoices: LiveHiddenUsers,
followUsers: Set<HexKey>,
): NoteComposeReportState {
LocalCache.appHost.assertNotMainThread()
val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex
val isFromLoggedInFollow = note.author?.let { followUsers.contains(it.pubkeyHex) } ?: true
val isPostHidden = note.isHiddenFor(accountChoices)