diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index f7a4cbd8e5..c0a1d68dcc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -137,6 +137,7 @@ import com.vitorpamplona.amethyst.service.uploads.FileHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.EventProcessor import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEntry import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent +import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId import com.vitorpamplona.quartz.concord.cord04Roles.ConcordPermissions import com.vitorpamplona.quartz.concord.cord04Roles.MetadataEntity import com.vitorpamplona.quartz.concord.cord04Roles.RoleEntity @@ -447,6 +448,35 @@ class Account( cache.consumeConcordRumor(communityId, channelIdHex, rumor) } + /** + * Copies each folded community's metadata (name/icon, channel flags, this account's + * membership) onto its [ConcordChannel] objects in the cache, and drops messages from + * authors banned since they loaded. Runs account-wide on every + * [com.vitorpamplona.amethyst.commons.model.concord.ConcordSessionManager] revision — + * NOT gated behind the Concord hub screen — so every surface (the Messages-tab + * community chip, the chat screen title) reflects the current fold, and bans apply, + * even when the hub was never opened. + */ + fun refreshConcordChannelIndex() { + val myPubKey = signer.pubKey + val relaysByCommunity = + concordChannelList.liveCommunities.value.associate { entry -> + entry.id to entry.relays.mapNotNullTo(mutableSetOf()) { RelayUrlNormalizer.normalizeOrNull(it) } + } + for (session in concordSessions.sessions()) { + val state = session.state.value ?: continue + val communityId = session.entry.id + val relays = relaysByCommunity[communityId] ?: emptySet() + for (channelIdHex in state.channels.keys) { + val channel = cache.getOrCreateConcordChannel(ConcordChannelId(communityId, channelIdHex)) + channel.updateFrom(state, relays, myPubKey) + channel.notes + .filter { _, note -> note.event?.pubKey?.let { state.authority.isBanned(it) } == true } + .forEach { channel.removeNote(it) } + } + } + } + val publicChatListDecryptionCache = PublicChatListDecryptionCache(signer) val publicChatList = PublicChatListState(signer, cache, publicChatListDecryptionCache, scope, settings) @@ -4713,6 +4743,13 @@ class Account( } } + // Keep Concord channel metadata (community name/icon, membership) live across the whole + // app — not just the hub screen — so the Messages tab renders each channel's community + // chip, and per-community bans apply, as soon as a Control Plane folds. + scope.launch { + concordSessions.revision.collect { refreshConcordChannelIndex() } + } + scope.launch { cache.antiSpam.flowSpam.collect { it.cache.spamMessages.snapshot().values.forEach { spammer -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelSubscription.kt index 1491b5befc..034c31d7ed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/datasource/ConcordChannelSubscription.kt @@ -26,16 +26,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer /** * Mount on any screen that lists the user's joined Concord Channels (the Messages * tab, the Concord home) to keep their planes live and their folded metadata in - * the [LocalCache] channel index. + * the LocalCache channel index. * * The query state is keyed on the account (stable), so the assembler wouldn't * re-run its filter derivation on its own when a community folds or the joined set @@ -57,39 +53,12 @@ fun ConcordChannelSubscription( val revision by account.concordSessions.revision.collectAsStateWithLifecycle() LaunchedEffect(revision) { - refreshConcordChannelIndex(account) + // The channel-index refresh (community name/icon, membership, ban pruning) runs + // account-wide from Account on this same revision, so the Messages tab has chips even + // when this screen was never opened. Here we only need to re-derive the subscription + // filters, since a newly-folded channel plane must now be subscribed. dataSource.invalidateFilters() } LifecycleAwareKeyDataSourceSubscription(state, dataSource) } - -/** - * Projects each folded community session into the shared LocalCache channel index - * so the Messages list and chat screens render an up-to-date [ConcordChannel] - * (name, voice/private flags, community name/relays, this account's membership). - */ -private fun refreshConcordChannelIndex(account: Account) { - val myPubKey = account.signer.pubKey - val relaysByCommunity = - account.concordChannelList.liveCommunities.value - .associate { entry -> - entry.id to entry.relays.mapNotNullTo(mutableSetOf()) { RelayUrlNormalizer.normalizeOrNull(it) } - } - - for (session in account.concordSessions.sessions()) { - val state = session.state.value ?: continue - val communityId = session.entry.id - val relays = relaysByCommunity[communityId] ?: emptySet() - for (channelIdHex in state.channels.keys) { - val channel = LocalCache.getOrCreateConcordChannel(ConcordChannelId(communityId, channelIdHex)) - channel.updateFrom(state, relays, myPubKey) - // A member banned since these notes loaded: drop their messages now (the - // ingest gate stops future ones). removeNote invalidates the feed, so the - // ban is reflected live rather than only on the next feed pass. - channel.notes - .filter { _, note -> note.event?.pubKey?.let { state.authority.isBanned(it) } == true } - .forEach { channel.removeNote(it) } - } - } -}