From f67d242f7a9b116ab58e47c06136f1c25b783870 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 21 Jul 2026 13:21:35 -0400 Subject: [PATCH 1/4] fix: show Concord channels on Messages as soon as the control plane folds A Concord control-plane fold is what first reveals a community's channels and makes ConcordCommunitySession.state non-null, without which ChatroomListKnownFeedFilter emits nothing at all for that community. None of it flows through LocalCache.newEventBundles, so the additive feed path could not see it: a folded channel only reached the Messages tab if a message for it happened to arrive afterwards. Cold boot therefore showed a subset of a community's channels, or omitted a quiet community entirely, until some unrelated invalidation fired. Measured on device: the Concord hub reported 3 communities / 17 channels folded in memory while Messages rendered 3 rows and omitted one community completely. AccountFeedContentStates already forces a rebuild for the Marmot, NIP-29, geohash, view-mode and pin flows for exactly this reason; Concord was the one missing collector. Add it, sampled the same way Account.kt samples this flow to drive refreshConcordChannelIndex. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../loggedIn/AccountFeedContentStates.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index e1808649a8..0183867e41 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -77,7 +77,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks.dal.WebBookmar import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.dal.WorkoutFeedFilter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch class AccountFeedContentStates( @@ -201,6 +203,26 @@ class AccountFeedContentStates( } } + // A Concord control-plane fold is what first reveals a community's channels (and what makes + // ConcordCommunitySession.state non-null, without which ChatroomListKnownFeedFilter emits + // nothing at all for that community). None of it flows through LocalCache.newEventBundles, + // so the additive path can't see it: a folded channel reaches the Messages tab only if a + // message for it happens to arrive afterwards. Cold boot therefore shows a *subset* of a + // community's channels, or omits a quiet community entirely, until some unrelated + // invalidation fires. Rebuild on every structural change instead. `revision` bumps only on + // fold/membership/rekey (never a plain message), and sample() coalesces the burst of folds + // that lands as each control plane catches up — the same pairing Account.kt uses to drive + // refreshConcordChannelIndex off this flow. + scope.launch(Dispatchers.IO) { + @OptIn(FlowPreview::class) + account.concordSessions.revision + .drop(1) + .sample(500) + .collect { + dmKnown.invalidateData() + } + } + // Same for the Concord view mode (inline channels vs one row per community). scope.launch(Dispatchers.IO) { account.settings.concordViewMode From 50025660a3a36a8f74c7ac5f96ce137393286e61 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 21 Jul 2026 13:21:54 -0400 Subject: [PATCH 2/4] perf: cut Concord revision churn and quadratic control-plane re-folds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cold boot bumped the session revision ~292 times for 3 communities, driving 22 Messages rebuilds and re-deriving every plane subscription each time. Three compounding causes, all measured on device: 1. Every refold republished state even when the fold was identical. ConcordCommunityState and its components were plain classes, so StateFlow conflation never applied and a prior-epoch wrap that didn't move the anti-rollback floor still counted as a change. Make the fold result compare by value (AuthorityResolver holds only immutable value fields; a data class with a private constructor is fine). 2. A control wrap bumped twice — once from ingest() returning STRUCTURAL and once from the per-session state watcher reacting to the same refold. Add ConcordIngestOutcome.STRUCTURAL_FOLD for the two control-plane branches so the manager leaves those to the watcher, which (given 1) now fires only on genuine change. Guestbook and base-rekey keep STRUCTURAL: they mutate members/the rekey buffer, not state, so no watcher covers them. 3. refold() and controlFloorsLocked() re-opened the WHOLE wrap buffer on every control wrap, and opening a wrap is a NIP-44 decrypt + parse — making a backfill quadratic in decryptions (~8.6k opens to ingest 93 wraps for one community). Memoize editions by wrap id: one open per wrap, ingest() stays synchronous and results are unchanged. Measured over one cold boot: revision bumps 292 -> 87, Messages rebuilds 22 -> 7, and time from first fold to all 17 channels 43s -> 7.5s. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../model/concord/ConcordCommunitySession.kt | 57 ++++++++++++++++--- .../model/concord/ConcordSessionManager.kt | 3 + .../concord/ConcordCommunitySessionTest.kt | 2 +- .../concord/ConcordSessionRegistryTest.kt | 2 +- .../cord02Community/ConcordCommunityState.kt | 4 +- .../concord/cord04Roles/AuthorityResolver.kt | 2 +- .../concord/cord04Roles/ControlEntities.kt | 10 ++-- 7 files changed, 63 insertions(+), 17 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordCommunitySession.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordCommunitySession.kt index 9bb65aef67..49acfd9b1e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordCommunitySession.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordCommunitySession.kt @@ -62,8 +62,17 @@ enum class ConcordIngestOutcome { * a chat/reaction/reply/delete message landing, or a duplicate wrap. Must NOT bump the revision. */ NON_STRUCTURAL, - /** Ours and changed structure: a Control-Plane fold (metadata/channels/membership/authority), a - * guestbook membership change, or a buffered base-rekey. Bumps the revision. */ + /** Ours and re-folded the Control Plane. The fold republishes [ConcordCommunitySession.state], + * so the session's own state watcher is what bumps the revision — and, because the folded state + * compares by value, only when the fold actually *changed* something. A control wrap that folds + * to an identical state (a prior-epoch wrap that doesn't move the anti-rollback floor, a role + * edition that touches nothing we subscribe on) therefore costs no bump at all. The manager must + * NOT bump on this outcome as well, or every control wrap counts twice. */ + STRUCTURAL_FOLD, + + /** Ours and changed structure *without* touching [ConcordCommunitySession.state]: a guestbook + * membership change (which republishes `members`) or a buffered base-rekey. No state watcher + * covers these, so the manager bumps the revision directly. */ STRUCTURAL, ; @@ -145,6 +154,22 @@ class ConcordCommunitySession( // Deduped inbound wraps. private val controlWraps = LinkedHashMap() + /** + * Decrypted control editions memoized by wrap id. + * + * Both [refold] and [controlFloorsLocked] fold their WHOLE buffer on every inbound control + * wrap, and turning a wrap into an edition is a NIP-44 open + parse. Re-deriving them each + * time made a cold-boot backfill quadratic in decryptions — one measured boot did ~8.6k opens + * to ingest 93 control wraps for a single community. Memoizing makes it one open per wrap. + * + * Wrap ids are unique and a wrap only ever belongs to one plane (it is routed by `pubKey`), so + * a single id-keyed map is safe across the current and prior-epoch Control Planes even though + * they open under different keys. A wrap that fails to open caches `null` so it is not retried + * on every subsequent fold. The wrap buffers are only ever added to, so this tracks their + * lifetime exactly and needs no separate eviction. + */ + private val editionByWrapId = HashMap() + // Prior-epoch Control Plane address -> (wrapId -> wrap). Kept apart from [controlWraps]: these // never join the live fold, they only produce the anti-rollback floor. private val historicalControlWraps = HashMap>() @@ -280,7 +305,7 @@ class ConcordCommunitySession( fun auxStreamKeys(): List = listOf(guestbookKey, nextBaseRekeyKey) /** The community's current Control Plane editions — the input a moderation edition chains onto. */ - fun controlEditions(): List = lock.withLock { ConcordActions.controlEditions(controlWraps.values.toList(), controlPlaneKey) } + fun controlEditions(): List = lock.withLock { editionsLocked(controlWraps.values.toList(), controlPlaneKey) } /** The raw Control Plane wraps buffered so far — the input a Refounding compacts (CORD-06 §3). */ fun controlPlaneWraps(): List = lock.withLock { controlWraps.values.toList() } @@ -314,7 +339,7 @@ class ConcordCommunitySession( if (controlWraps.put(wrap.id, wrap) != null) return ConcordIngestOutcome.NON_STRUCTURAL // dup } refold() - return ConcordIngestOutcome.STRUCTURAL + return ConcordIngestOutcome.STRUCTURAL_FOLD } guestbookAddress -> { lock.withLock { @@ -341,7 +366,7 @@ class ConcordCommunitySession( if (buffer.put(wrap.id, wrap) != null) return ConcordIngestOutcome.NON_STRUCTURAL // dup } refold() - return ConcordIngestOutcome.STRUCTURAL + return ConcordIngestOutcome.STRUCTURAL_FOLD } val current = lock.withLock { channelKeysByAddress[wrap.pubKey] } if (current != null) { @@ -422,7 +447,7 @@ class ConcordCommunitySession( val wraps = controlWraps.values.toList() val folded = ConcordCommunityState.fold( - ConcordActions.controlEditions(wraps, controlPlaneKey), + editionsLocked(wraps, controlPlaneKey), entry.owner, controlFloorsLocked(), ) @@ -455,6 +480,24 @@ class ConcordCommunitySession( for (channelIdHex in newChannels) reprojectChannel(channelIdHex) } + /** + * [wraps] opened into editions through [editionByWrapId], so a wrap is only ever decrypted + * once no matter how many folds it participates in. Caller must hold [lock]. + */ + private fun editionsLocked( + wraps: Collection, + planeKey: GroupKey, + ): List = + wraps.mapNotNull { wrap -> + if (editionByWrapId.containsKey(wrap.id)) { + editionByWrapId[wrap.id] + } else { + val edition = ConcordStreamEnvelope.openOrNull(wrap, planeKey)?.let { ControlEdition.fromRumor(it.rumor) } + editionByWrapId[wrap.id] = edition + edition + } + } + /** * The per-entity anti-rollback floor: the authority-gated heads of every prior epoch's * Control Plane we still hold a root for, folded **oldest epoch first** so each epoch is @@ -472,7 +515,7 @@ class ConcordCommunitySession( var floors = emptyMap() for ((address, keyAtEpoch) in historicalControlKeys.entries.sortedBy { it.value.second }) { val wraps = historicalControlWraps[address]?.values?.toList() ?: continue - val editions = ConcordActions.controlEditions(wraps, keyAtEpoch.first) + val editions = editionsLocked(wraps, keyAtEpoch.first) if (editions.isEmpty()) continue floors = ConcordCommunityState.authorizedHeads(editions, entry.owner, floors) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordSessionManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordSessionManager.kt index bd2775eef2..0bb8faba0f 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordSessionManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordSessionManager.kt @@ -150,6 +150,9 @@ class ConcordSessionManager( seenOnRelays: Set = emptySet(), ): Boolean { val outcome = registry.ingest(wrap, seenOnRelays) + // Only the planes that change structure *without* republishing `state`. A control-plane + // fold returns STRUCTURAL_FOLD and is bumped by the per-session state watcher instead, which + // (since the folded state compares by value) fires only when the fold genuinely changed. if (outcome == ConcordIngestOutcome.STRUCTURAL) bumpRevision() return outcome.claimed } diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordCommunitySessionTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordCommunitySessionTest.kt index 0c0afcfd84..7f7882a14d 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordCommunitySessionTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordCommunitySessionTest.kt @@ -108,7 +108,7 @@ class ConcordCommunitySessionTest { // Feed the genesis control wraps → state folds, channels + membership resolve. A fold is // STRUCTURAL (it moves the subscription set), so it's allowed to bump the revision. - community.genesisWraps.forEach { assertEquals(ConcordIngestOutcome.STRUCTURAL, session.ingest(it)) } + community.genesisWraps.forEach { assertEquals(ConcordIngestOutcome.STRUCTURAL_FOLD, session.ingest(it)) } val state = session.state.value assertEquals("Nostrichs", state?.metadata?.name) assertTrue(state!!.channels.containsKey(community.generalChannelIdHex)) diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordSessionRegistryTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordSessionRegistryTest.kt index e292d55451..0419520238 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordSessionRegistryTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/concord/ConcordSessionRegistryTest.kt @@ -70,7 +70,7 @@ class ConcordSessionRegistryTest { assertTrue(registry.subscribeAddresses().contains(beta.controlPlane.publicKeyHex)) // A genesis control wrap routes to Alpha's session and folds it (STRUCTURAL). - alpha.genesisWraps.forEach { assertEquals(ConcordIngestOutcome.STRUCTURAL, registry.ingest(it)) } + alpha.genesisWraps.forEach { assertEquals(ConcordIngestOutcome.STRUCTURAL_FOLD, registry.ingest(it)) } val alphaState = registry.sessionFor(alpha.communityIdHex)!!.state.value assertEquals("Alpha", alphaState?.metadata?.name) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord02Community/ConcordCommunityState.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord02Community/ConcordCommunityState.kt index da4b6c817a..31c74caa7e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord02Community/ConcordCommunityState.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord02Community/ConcordCommunityState.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.concord.cord04Roles.RoleEntity import com.vitorpamplona.quartz.concord.cord04Roles.asFloor /** A channel id paired with its current folded definition. */ -class ConcordChannel( +data class ConcordChannel( val channelIdHex: String, val definition: ChannelEntity, ) @@ -49,7 +49,7 @@ class ConcordChannel( * "Every member keeps the entire Control Plane in sync — it is small and must * stay complete." Recompute this whenever the known editions change. */ -class ConcordCommunityState( +data class ConcordCommunityState( val ownerPubKey: String, val metadata: MetadataEntity?, val channels: Map, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord04Roles/AuthorityResolver.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord04Roles/AuthorityResolver.kt index 8f67a24b5a..04fa0176c2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord04Roles/AuthorityResolver.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord04Roles/AuthorityResolver.kt @@ -44,7 +44,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey * assigned Role and holds [ConcordPermissions.MANAGE_ROLES]. Cycles that never * touch the owner can never bootstrap themselves. */ -class AuthorityResolver private constructor( +data class AuthorityResolver private constructor( private val ownerLower: String, private val roles: Map, private val memberRoles: Map>, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord04Roles/ControlEntities.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord04Roles/ControlEntities.kt index adacb499e6..71696ba8c9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord04Roles/ControlEntities.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord04Roles/ControlEntities.kt @@ -64,7 +64,7 @@ object ConcordJson { * drops the role, and with it every authority (grant) that depends on it. */ @Serializable -class RoleScope( +data class RoleScope( val kind: String = "server", @SerialName("channel_id") val channelId: String? = null, ) @@ -75,7 +75,7 @@ class RoleScope( * ranks higher; no role may claim position 0 (reserved for the owner). */ @Serializable -class RoleEntity( +data class RoleEntity( val name: String = "", val position: Long = 0, /** u64 permission bitfield as a decimal string. */ @@ -94,7 +94,7 @@ class RoleEntity( * terminates at the owner (see [AuthorityResolver]). */ @Serializable -class GrantEntity( +data class GrantEntity( val member: String = "", @SerialName("role_ids") val roleIds: List = emptyList(), ) @@ -105,7 +105,7 @@ class GrantEntity( * A [deleted] channel is terminal — its id is never reused. */ @Serializable -class ChannelEntity( +data class ChannelEntity( val name: String = "", val private: Boolean = false, val voice: Boolean = false, @@ -122,7 +122,7 @@ class ChannelEntity( * community name too. */ @Serializable -class MetadataEntity( +data class MetadataEntity( val name: String = "", val icon: ImagePointer? = null, val banner: ImagePointer? = null, From 1fee674350a1debb9c663f463dbb6ed7db5e53d3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 21 Jul 2026 13:22:06 -0400 Subject: [PATCH 3/4] fix: keep event-less placeholder rooms when a deletion arrives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The additive feed path re-filters the existing list whenever an incoming batch contains a kind-5, dropping notes whose event has been deleted. Event-less notes fell into the else branch and returned false, so they were dropped too. An event-less row is a placeholder the filter synthesizes for a room with no message yet — a just-joined Concord channel, NIP-29 group, Marmot group or geohash cell. It carries no event, so it cannot have been deleted. Dropping it removed every such row from Messages the moment ANY unrelated deletion landed, and because this is the additive path the rows stayed gone until the next full rebuild. A community whose channels are all quiet looked like it had never loaded at all. Verified on device: surviving Concord placeholders in sort() went 0 -> 14, and a community that had been absent from Messages entirely now renders all of its channels. Not Concord-specific — the same placeholderNote() pattern backs NIP-29, Marmot and geohash rooms. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../amethyst/commons/ui/feeds/FeedContentState.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/FeedContentState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/FeedContentState.kt index bc6eae3baf..9945c77fd0 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/FeedContentState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/FeedContentState.kt @@ -161,7 +161,14 @@ class FeedContentState( if (noteEvent != null) { !cacheProvider.hasBeenDeleted(noteEvent) } else { - false + // An event-less row is a placeholder the filter synthesized for a room + // that has no message yet — a just-joined Concord channel, NIP-29 group, + // Marmot group or geohash cell. It carries no event, so it cannot have + // been deleted, and dropping it here deleted every such row from the + // Messages list the moment ANY kind-5 landed in an unrelated batch. The + // row then stayed gone until the next full rebuild, which is why a quiet + // community looked like it had never loaded at all. + true } }.toImmutableList() } From 9ddf571b1139d2ff9523f39beac9550b2bcde8d9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 21 Jul 2026 14:09:59 -0400 Subject: [PATCH 4/4] fix: persist the Concord community list so cold boot doesn't refetch it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AccountSettings has held backupConcordList and saved it on change since the feature landed, but the field was never wired into LocalPreferences: it was neither written to nor read back from the encrypted prefs. So it only ever existed for the lifetime of the process, concordList() returned null on every cold boot, and the kind-13302 joined-communities list had to be refetched from relays before a single Concord plane could be subscribed. Every sibling list — channel, community, hashtag, geohash, ephemeral chat, relay group, trust provider — is persisted this way; Concord was the one that was missed. That made it the only chat type whose rooms could not appear until the network answered, which is the bulk of the cold-boot delay: the joined list gates the control-plane REQ, the control plane gates the fold, and the fold gates the channels. Measured on device, boot -> first Concord plane wrap: - without the backup: liveCommunities sat empty for ~56 s waiting on the 13302 fetch (first arrival from nostr.mom), first wrap at +45 s - with the backup restored: list decoded 1.6 s after boot (30 ms), first wrap at +6 s Wired the same five sites the other lists use (pref key, save, read, parse, restore). Prefs are encrypted and backupCashuWallet already sets the precedent for persisting a secret-bearing event, so the community roots in the 13302 content are stored no differently than the wallet's. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../java/com/vitorpamplona/amethyst/LocalPreferences.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 9603f78862..f489142661 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.model.UiSettings import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent import com.vitorpamplona.quartz.nip01Core.core.Event @@ -168,6 +169,7 @@ private object PrefKeys { const val LATEST_GEOHASH_LIST = "latestGeohashList" const val LATEST_EPHEMERAL_LIST = "latestEphemeralChatList" const val LATEST_RELAY_GROUP_LIST = "latestRelayGroupList" + const val LATEST_CONCORD_LIST = "latestConcordList" const val LATEST_TRUST_PROVIDER_LIST = "latestTrustProviderList" const val CALLS_ENABLED = "calls_enabled" const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog" @@ -577,6 +579,7 @@ object LocalPreferences { putOrRemove(PrefKeys.LATEST_GEOHASH_LIST, settings.backupGeohashList) putOrRemove(PrefKeys.LATEST_EPHEMERAL_LIST, settings.backupEphemeralChatList) putOrRemove(PrefKeys.LATEST_RELAY_GROUP_LIST, settings.backupRelayGroupList) + putOrRemove(PrefKeys.LATEST_CONCORD_LIST, settings.backupConcordList) putOrRemove(PrefKeys.LATEST_TRUST_PROVIDER_LIST, settings.backupTrustProviderList) putOrRemove(PrefKeys.LATEST_PAYMENT_TARGETS, settings.backupNipA3PaymentTargets) putOrRemove(PrefKeys.LATEST_CASHU_WALLET, settings.backupCashuWallet) @@ -763,6 +766,7 @@ object LocalPreferences { val latestGeohashListStr = getString(PrefKeys.LATEST_GEOHASH_LIST, null) val latestEphemeralListStr = getString(PrefKeys.LATEST_EPHEMERAL_LIST, null) val latestRelayGroupListStr = getString(PrefKeys.LATEST_RELAY_GROUP_LIST, null) + val latestConcordListStr = getString(PrefKeys.LATEST_CONCORD_LIST, null) val latestTrustProviderListStr = getString(PrefKeys.LATEST_TRUST_PROVIDER_LIST, null) val latestPaymentTargetsStr = getString(PrefKeys.LATEST_PAYMENT_TARGETS, null) val latestCashuWalletStr = getString(PrefKeys.LATEST_CASHU_WALLET, null) @@ -823,6 +827,7 @@ object LocalPreferences { val latestGeohashList = async { parseEventOrNull(latestGeohashListStr) } val latestEphemeralList = async { parseEventOrNull(latestEphemeralListStr) } val latestRelayGroupList = async { parseEventOrNull(latestRelayGroupListStr) } + val latestConcordList = async { parseEventOrNull(latestConcordListStr) } val latestTrustProviderList = async { parseEventOrNull(latestTrustProviderListStr) } val latestPaymentTargets = async { parseEventOrNull(latestPaymentTargetsStr) } val latestCashuWallet = @@ -875,6 +880,7 @@ object LocalPreferences { val latestGeohashListResolved = latestGeohashList.await() val latestEphemeralListResolved = latestEphemeralList.await() val latestRelayGroupListResolved = latestRelayGroupList.await() + val latestConcordListResolved = latestConcordList.await() val latestTrustProviderListResolved = latestTrustProviderList.await() val latestPaymentTargetsResolved = latestPaymentTargets.await() val latestCashuWalletResolved = latestCashuWallet.await() @@ -969,6 +975,7 @@ object LocalPreferences { backupGeohashList = latestGeohashListResolved, backupEphemeralChatList = latestEphemeralListResolved, backupRelayGroupList = latestRelayGroupListResolved, + backupConcordList = latestConcordListResolved, backupTrustProviderList = latestTrustProviderListResolved, lastReadPerRoute = MutableStateFlow(lastReadPerRouteResolved), hasDonatedInVersion = MutableStateFlow(hasDonatedInVersion),