diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index e18e0779dd..a4d54a6a3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -788,6 +788,62 @@ class AccountSettings( // list names // --- + /** + * All per-screen persisted feed filters paired with their factory default. + * Deleting a list (NIP-51 people list / follow pack) must reset any screen whose + * filter still points at the deleted address — otherwise the screen keeps a + * dangling [TopFilter.PeopleList] that re-creates an empty AddressableNote shell + * on every start and shows the list's dTag/UUID in the top bar instead of a name. + */ + private val feedFiltersWithDefaults: List, TopFilter>> = + listOf( + defaultHomeFollowList to TopFilter.AllFollows, + defaultStoriesFollowList to TopFilter.Global, + defaultNotificationFollowList to TopFilter.Selected, + defaultDiscoveryFollowList to TopFilter.Global, + defaultPollsFollowList to TopFilter.Global, + defaultPicturesFollowList to TopFilter.Global, + defaultNappletsFollowList to TopFilter.Global, + defaultNsitesFollowList to TopFilter.Global, + defaultWorkoutsFollowList to TopFilter.Global, + defaultGitRepositoriesFollowList to TopFilter.Global, + defaultHighlightsFollowList to TopFilter.Global, + defaultCalendarsFollowList to TopFilter.Global, + defaultProductsFollowList to TopFilter.AroundMe, + defaultShortsFollowList to TopFilter.Global, + defaultPublicChatsFollowList to TopFilter.Global, + defaultLiveStreamsFollowList to TopFilter.Global, + defaultNestsFollowList to TopFilter.Global, + defaultLongsFollowList to TopFilter.Global, + defaultArticlesFollowList to TopFilter.AllFollows, + defaultMusicTracksFollowList to TopFilter.Global, + defaultMusicPlaylistsFollowList to TopFilter.Global, + defaultPodcastEpisodesFollowList to TopFilter.Global, + defaultPodcastsFollowList to TopFilter.Global, + defaultSoftwareAppsFollowList to TopFilter.Global, + defaultBadgesFollowList to TopFilter.Mine, + defaultBrowseEmojiSetsFollowList to TopFilter.Global, + defaultCommunitiesFollowList to TopFilter.AllFollows, + defaultFollowPacksFollowList to TopFilter.Global, + defaultAppRecommendationsFollowList to TopFilter.Global, + defaultRelayGroupsDiscoveryFollowList to TopFilter.Mine, + ) + + /** Resets every persisted feed filter that points at the deleted list's address. */ + fun resetFeedFiltersPointingTo(address: Address) { + var changed = false + + feedFiltersWithDefaults.forEach { (flow, default) -> + val current = flow.value + if (current is TopFilter.AddressableTopFilter && current.address == address) { + flow.tryEmit(default) + changed = true + } + } + + if (changed) saveAccountSettings() + } + fun changeDefaultHomeFollowList(name: FeedDefinition) { changeDefaultHomeFollowList(name.code) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 4d64a94cdc..7342980097 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -825,6 +825,12 @@ object LocalCache : ILocalCache, ICacheProvider, Dao { false } + /** + * Checks if a kind-5 event from the addressable's own author has deleted this + * address. Works for empty addressable shells whose event is not loaded yet. + */ + fun hasBeenDeleted(address: Address): Boolean = deletionIndex.hasBeenDeleted(address, address.pubKeyHex) + fun getOrAddAliasNote( idHex: String, note: Note, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt index 362d78b462..8cbb73fed9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt @@ -71,7 +71,15 @@ class FollowListsState( ) { val user = cache.getOrCreateUser(signer.pubKey) - fun existingPeopleListNotes() = cache.addressables.filter(FollowListEvent.KIND, user.pubkeyHex) + // Hides shells that a kind-5 deletion event from the list's author has already + // deleted (e.g. a persisted TopFilter re-creates an empty shell for the deleted + // address after a restart, and its name falls back to the dTag/UUID). Shells that + // are merely not loaded yet stay in the list so the UI can subscribe and fetch + // them from relays. + fun existingPeopleListNotes() = + cache.addressables + .filter(FollowListEvent.KIND, user.pubkeyHex) + .filter { it.event != null || !cache.hasBeenDeleted(it.address) } val followListVersions = MutableStateFlow(0) @@ -255,6 +263,10 @@ class FollowListsState( val followListEvent = getPeopleList(identifierTag) val deletionEvent = account.signer.sign(DeletionEvent.build(listOf(followListEvent))) account.sendMyPublicAndPrivateOutbox(deletionEvent) + // Any screen whose persisted feed filter still points at this follow pack would + // keep re-creating an empty shell for its address (and render the dTag/UUID in + // the top bar) — reset those filters to their default. + account.settings.resetFeedFiltersPointingTo(followListEvent.address()) } suspend fun addUserToSet( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt index d3e224650d..f27f43c9e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt @@ -69,10 +69,17 @@ class PeopleListsState( ) { val user = cache.getOrCreateUser(signer.pubKey) + // Hides the fixed-dTag block-list shell when it is not loaded (it has no + // meaningful name until it exists) and shells that a kind-5 deletion event from + // the list's author has already deleted (e.g. a persisted TopFilter re-creates an + // empty shell for the deleted address after a restart, and its name falls back to + // the dTag/UUID). Shells that are merely not loaded yet stay in the list so the UI + // can subscribe and fetch them from relays. fun existingPeopleListNotes() = cache.addressables .filter(PeopleListEvent.KIND, user.pubkeyHex) .filter { it.dTag() != PeopleListEvent.BLOCK_LIST_D_TAG || it.event != null } + .filter { it.event != null || !cache.hasBeenDeleted(it.address) } val peopleListVersions = MutableStateFlow(0) @@ -262,6 +269,10 @@ class PeopleListsState( val followListEvent = getPeopleList(identifierTag) val deletionEvent = account.signer.sign(DeletionEvent.build(listOf(followListEvent))) account.sendMyPublicAndPrivateOutbox(deletionEvent) + // Any screen whose persisted feed filter still points at this list would keep + // re-creating an empty shell for its address (and render the dTag/UUID in the + // top bar) — reset those filters to their default. + account.settings.resetFeedFiltersPointingTo(followListEvent.address()) } suspend fun addUserToSet( diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip09Deletions/DeletionIndex.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip09Deletions/DeletionIndex.kt index 3be2157748..a26c716af2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip09Deletions/DeletionIndex.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip09Deletions/DeletionIndex.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.nip09Deletions +import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -115,6 +116,18 @@ class DeletionIndex { return false } + /** + * Checks if a kind-5 event signed by [pubKey] deleted the addressable at [address]. + * Used when the addressable's event is not loaded yet (e.g. an empty shell + * re-created after a restart), so there is no event id or createdAt to compare + * against. A re-published version would arrive as a loaded event and is checked + * by [hasBeenDeleted] instead. + */ + fun hasBeenDeleted( + address: Address, + pubKey: HexKey, + ): Boolean = hasBeenDeleted(DeletionRequest(address.toValue(), pubKey)) + private fun hasBeenDeleted(key: DeletionRequest) = deletedReferencesBefore.containsKey(key) private fun hasBeenDeleted(