From 36d1b9b506f84365b0f6f9750145de0e7e4f3374 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 15:00:53 +0000 Subject: [PATCH 1/3] feat: split notifications Global into Selected (curated) and a raw Global The notifications mode previously labeled Global applied per-kind relevance heuristics (tagsAnEventByUser) that drop reactions/reposts targeting other people's notes, unrelated thread replies, etc. That mode is now shown as "Selected" in the notifications spinner, and a new real Global mode (TopFilter.GlobalRaw) shows every event that p-tags the user, filtered only by the notification kind whitelist, hidden/reported authors, muted threads, muted DM content, and self-authored events. The split-notifications Everyone tab now pins to the raw Global mode. Other feeds' Global filter is unchanged. https://claude.ai/code/session_013VDWpD8Dr6sBF7tBEUZGpg --- .../NotificationFeedFilterModeOverrideTest.kt | 35 +++++++++++++------ .../amethyst/model/AccountSettings.kt | 9 +++++ .../topNavFeeds/FeedTopNavFilterState.kt | 2 +- .../navigation/topbars/FeedFilterSpinner.kt | 5 +++ .../amethyst/ui/screen/TopNavFilterState.kt | 33 +++++++++++++++++ .../loggedIn/AccountFeedContentStates.kt | 2 +- .../notifications/NotificationTopBar.kt | 2 +- .../dal/NotificationFeedFilter.kt | 8 +++-- amethyst/src/main/res/values/strings.xml | 1 + 9 files changed, 82 insertions(+), 15 deletions(-) diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/NotificationFeedFilterModeOverrideTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/NotificationFeedFilterModeOverrideTest.kt index 6114b9efe5..ad05ed22a6 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/NotificationFeedFilterModeOverrideTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/NotificationFeedFilterModeOverrideTest.kt @@ -55,7 +55,7 @@ import org.junit.runner.RunWith * 1. `feedKey` is mode-discriminated so each pinned tab caches independently. * 2. `followList()` honors `modeOverride` when set; falls back to the spinner setting otherwise. * 3. `buildFilterParams()` returns a GlobalTopNavFilter-backed FilterByListParams for - * `TopFilter.Global` (so `isGlobal()` is true, allowing non-follower notifications through), + * `TopFilter.GlobalRaw` (so `isGlobal()` is true, allowing non-follower notifications through), * and a non-Global filter for `TopFilter.AllFollows` (forcing the follow-membership gate). */ @RunWith(AndroidJUnit4::class) @@ -96,7 +96,7 @@ class NotificationFeedFilterModeOverrideTest { fun feedKeyDiffersByModeOverride() { val spinner = NotificationFeedFilter(account) val following = NotificationFeedFilter(account, TopFilter.AllFollows) - val everyone = NotificationFeedFilter(account, TopFilter.Global) + val everyone = NotificationFeedFilter(account, TopFilter.GlobalRaw) assertNotEquals( "Following tab's feedKey must differ from Everyone tab's so each caches independently", @@ -104,8 +104,8 @@ class NotificationFeedFilterModeOverrideTest { everyone.feedKey(), ) assertTrue( - "Everyone feedKey should encode the Global filter code", - everyone.feedKey().endsWith(TopFilter.Global.code), + "Everyone feedKey should encode the GlobalRaw filter code", + everyone.feedKey().endsWith(TopFilter.GlobalRaw.code), ) assertTrue( "Following feedKey should encode the AllFollows filter code", @@ -113,17 +113,17 @@ class NotificationFeedFilterModeOverrideTest { ) // When override is null, feedKey reflects the spinner-selected default. - account.settings.defaultNotificationFollowList.value = TopFilter.Global + account.settings.defaultNotificationFollowList.value = TopFilter.GlobalRaw assertEquals(everyone.feedKey(), spinner.feedKey()) } @Test fun followListHonorsModeOverride() { val following = NotificationFeedFilter(account, TopFilter.AllFollows) - val everyone = NotificationFeedFilter(account, TopFilter.Global) + val everyone = NotificationFeedFilter(account, TopFilter.GlobalRaw) assertEquals(TopFilter.AllFollows, following.followList()) - assertEquals(TopFilter.Global, everyone.followList()) + assertEquals(TopFilter.GlobalRaw, everyone.followList()) } @Test @@ -139,12 +139,27 @@ class NotificationFeedFilterModeOverrideTest { @Test fun buildFilterParamsForGlobalOverrideReportsGlobal() { - val everyone = NotificationFeedFilter(account, TopFilter.Global) + val selected = NotificationFeedFilter(account, TopFilter.Global) + + val params = selected.buildFilterParams(account) + + // isGlobal() short-circuits the follow-membership gate in acceptableEvent, + // which is how the Selected mode admits notifications from non-followed authors. + assertTrue( + "Selected mode's FilterByListParams must report isGlobal so non-followers pass the gate", + params.isGlobal(), + ) + } + + @Test + fun buildFilterParamsForGlobalRawOverrideReportsGlobal() { + val everyone = NotificationFeedFilter(account, TopFilter.GlobalRaw) val params = everyone.buildFilterParams(account) - // isGlobal() short-circuits the follow-membership gate in acceptableEvent, - // which is how the Everyone tab admits notifications from non-followed authors. + // GlobalRaw rides the same GlobalFeedFlow relay set as Global, so it must + // also report isGlobal and let non-followers through; the only difference + // is that acceptableEvent skips the per-kind relevance heuristics. assertTrue( "Everyone tab's FilterByListParams must report isGlobal so non-followers pass the gate", params.isGlobal(), 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 13679ab480..64e89cc2fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -96,6 +96,15 @@ sealed class TopFilter( @Serializable object Global : TopFilter(" Global ") + /** + * Global without curation heuristics. Used by Notifications to show every + * event that p-tags the user, removing only hidden/reported authors and + * user-curated mutes — unlike [Global], which in Notifications (shown as + * "Selected") also applies per-kind relevance rules. + */ + @Serializable + object GlobalRaw : TopFilter(" Global Raw ") + @Serializable object AllFollows : TopFilter(" All Follows ") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt index 0292c1f151..a217c984c0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt @@ -72,7 +72,7 @@ class FeedTopNavFilterState( ) { fun loadFlowsFor(listName: TopFilter): IFeedFlowsType = when (listName) { - TopFilter.Global -> { + TopFilter.Global, TopFilter.GlobalRaw -> { GlobalFeedFlow(followsRelays, proxyRelays, relayFeeds) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt index 1afd463fdb..45b65cfb1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt @@ -367,6 +367,7 @@ private fun FeedDefinition.group(): FeedGroup = when (code) { is TopFilter.AroundMe -> FeedGroup.LOCATIONS is TopFilter.Global -> FeedGroup.RELAYS + is TopFilter.GlobalRaw -> FeedGroup.RELAYS is TopFilter.AllFavoriteAlgoFeeds -> FeedGroup.DVMS else -> FeedGroup.FEEDS } @@ -507,6 +508,10 @@ private fun FeedIcon( MaterialSymbols.Public } + is TopFilter.GlobalRaw -> { + MaterialSymbols.Public + } + is TopFilter.AroundMe -> { MaterialSymbols.LocationOn } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt index 666c1e153b..f0c79c3688 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt @@ -82,6 +82,20 @@ class TopNavFilterState( name = ResourceName(R.string.follow_list_global), ) + // Notifications-only pair: the curated mode (TopFilter.Global) is shown as + // "Selected" while the raw every-p-tag mode takes the "Global" label. + val selectedFollow = + FeedDefinition( + code = TopFilter.Global, + name = ResourceName(R.string.follow_list_selected), + ) + + val globalRawFollow = + FeedDefinition( + code = TopFilter.GlobalRaw, + name = ResourceName(R.string.follow_list_global), + ) + val aroundMe = FeedDefinition( code = TopFilter.AroundMe, @@ -108,6 +122,8 @@ class TopNavFilterState( val defaultLists = persistentListOf(allFollows, userFollows, kind3Follows, aroundMe, globalFollow, muteListFollow) + val defaultNotificationLists = persistentListOf(allFollows, userFollows, kind3Follows, aroundMe, selectedFollow, globalRawFollow, muteListFollow) + fun mergePeopleLists( peopleLists: List, followLists: List, @@ -294,6 +310,18 @@ class TopNavFilterState( ) } + private val _notificationLists = + livePeopleListsFlow.transform { peopleLists -> + checkNotInMainThread() + emit( + listOf( + listOf(allFollows, userFollows, kind3Follows, aroundMe, selectedFollow, globalRawFollow), + peopleLists, + listOf(muteListFollow), + ).flatten().toImmutableList(), + ) + } + val kind3GlobalPeopleRoutes = _kind3GlobalPeopleRoutes .flowOn(Dispatchers.IO) @@ -304,6 +332,11 @@ class TopNavFilterState( .flowOn(Dispatchers.IO) .stateIn(scope, SharingStarted.Eagerly, defaultLists) + val notificationLists = + _notificationLists + .flowOn(Dispatchers.IO) + .stateIn(scope, SharingStarted.Eagerly, defaultNotificationLists) + val badgeRoutes = _badgeRoutes .flowOn(Dispatchers.IO) 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 2471d0dbb1..89e9c68523 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 @@ -127,7 +127,7 @@ class AccountFeedContentStates( val notifications = CardFeedContentState(NotificationFeedFilter(account), scope) val notificationsFollowing = CardFeedContentState(NotificationFeedFilter(account, TopFilter.AllFollows), scope) - val notificationsEveryone = CardFeedContentState(NotificationFeedFilter(account, TopFilter.Global), scope) + val notificationsEveryone = CardFeedContentState(NotificationFeedFilter(account, TopFilter.GlobalRaw), scope) val notificationsOpenPolls = OpenPollsState(account, scope) val notificationSummary = NotificationSummaryState(account) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationTopBar.kt index 781076c58e..94ba0b0101 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationTopBar.kt @@ -64,7 +64,7 @@ private fun TopNavFilterBar( accountViewModel: AccountViewModel, onChange: (FeedDefinition) -> Unit, ) { - val allLists by followListsModel.kind3GlobalPeople.collectAsStateWithLifecycle() + val allLists by followListsModel.notificationLists.collectAsStateWithLifecycle() FeedFilterSpinner( placeholderCode = listName, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index 4531066f1b..a27b0e2080 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -328,13 +328,17 @@ class NotificationFeedFilter( // Chess events bypass the follow filter — opponents may not be followed val isChessEvent = noteEvent is LiveChessGameAcceptEvent || noteEvent is LiveChessMoveEvent + // Raw global keeps every event that p-tags the user, skipping the + // per-kind relevance heuristics that the Selected mode applies. + val isRawGlobal = followList() is TopFilter.GlobalRaw + return noteEvent?.kind in NOTIFICATION_KINDS && (noteEvent is LnZapEvent || notifAuthor != loggedInUserHex) && - (isChessEvent || filterParams.isGlobal() || notifAuthor == null || filterParams.isAuthorInFollows(notifAuthor)) && + (isRawGlobal || isChessEvent || filterParams.isGlobal() || notifAuthor == null || filterParams.isAuthorInFollows(notifAuthor)) && noteEvent?.isTaggedUser(loggedInUserHex) ?: false && (filterParams.isHiddenList || notifAuthor == null || !account.isHidden(notifAuthor)) && (noteEvent !is PrivateDmEvent || !account.isDecryptedContentHidden(noteEvent)) && - tagsAnEventByUser(it, loggedInUserHex) + (isRawGlobal || tagsAnEventByUser(it, loggedInUserHex)) } override fun sort(items: Set): List = items.sortedWith(DefaultFeedOrder) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 5a77c85a7c..773149cc43 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1162,6 +1162,7 @@ Follows via Proxy Around Me Global + Selected Chess Mine Mute List From 3fc19ce0238790bde10c673a152a821d4c8e6524 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 15:15:16 +0000 Subject: [PATCH 2/3] refactor: model the curated notification mode as TopFilter.Selected Replaces TopFilter.GlobalRaw: TopFilter.Global itself now shows every event that p-tags the user in Notifications (still minus hidden/reported authors, muted threads, hidden DM content, and self), and the curated per-kind relevance heuristics move to a new notifications-only TopFilter.Selected mode. Selected is offered only in the notifications top nav filter and is the default for new users and new installs. Existing users who had Global selected keep Global and therefore now see everything, intentionally. https://claude.ai/code/session_013VDWpD8Dr6sBF7tBEUZGpg --- .../NotificationFeedFilterModeOverrideTest.kt | 46 ++++++++++--------- .../amethyst/LocalPreferences.kt | 2 +- .../amethyst/model/AccountSettings.kt | 13 +++--- .../topNavFeeds/FeedTopNavFilterState.kt | 2 +- .../navigation/topbars/FeedFilterSpinner.kt | 6 +-- .../amethyst/ui/screen/TopNavFilterState.kt | 16 ++----- .../loggedIn/AccountFeedContentStates.kt | 2 +- .../dal/NotificationFeedFilter.kt | 8 ++-- 8 files changed, 46 insertions(+), 49 deletions(-) diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/NotificationFeedFilterModeOverrideTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/NotificationFeedFilterModeOverrideTest.kt index ad05ed22a6..4392be5a34 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/NotificationFeedFilterModeOverrideTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/NotificationFeedFilterModeOverrideTest.kt @@ -54,9 +54,10 @@ import org.junit.runner.RunWith * Asserts the three contracts the feature relies on: * 1. `feedKey` is mode-discriminated so each pinned tab caches independently. * 2. `followList()` honors `modeOverride` when set; falls back to the spinner setting otherwise. - * 3. `buildFilterParams()` returns a GlobalTopNavFilter-backed FilterByListParams for - * `TopFilter.GlobalRaw` (so `isGlobal()` is true, allowing non-follower notifications through), - * and a non-Global filter for `TopFilter.AllFollows` (forcing the follow-membership gate). + * 3. `buildFilterParams()` returns a GlobalTopNavFilter-backed FilterByListParams for both + * `TopFilter.Global` and `TopFilter.Selected` (so `isGlobal()` is true, allowing + * non-follower notifications through), and a non-Global filter for `TopFilter.AllFollows` + * (forcing the follow-membership gate). */ @RunWith(AndroidJUnit4::class) class NotificationFeedFilterModeOverrideTest { @@ -96,7 +97,7 @@ class NotificationFeedFilterModeOverrideTest { fun feedKeyDiffersByModeOverride() { val spinner = NotificationFeedFilter(account) val following = NotificationFeedFilter(account, TopFilter.AllFollows) - val everyone = NotificationFeedFilter(account, TopFilter.GlobalRaw) + val everyone = NotificationFeedFilter(account, TopFilter.Global) assertNotEquals( "Following tab's feedKey must differ from Everyone tab's so each caches independently", @@ -104,8 +105,8 @@ class NotificationFeedFilterModeOverrideTest { everyone.feedKey(), ) assertTrue( - "Everyone feedKey should encode the GlobalRaw filter code", - everyone.feedKey().endsWith(TopFilter.GlobalRaw.code), + "Everyone feedKey should encode the Global filter code", + everyone.feedKey().endsWith(TopFilter.Global.code), ) assertTrue( "Following feedKey should encode the AllFollows filter code", @@ -113,25 +114,25 @@ class NotificationFeedFilterModeOverrideTest { ) // When override is null, feedKey reflects the spinner-selected default. - account.settings.defaultNotificationFollowList.value = TopFilter.GlobalRaw + account.settings.defaultNotificationFollowList.value = TopFilter.Global assertEquals(everyone.feedKey(), spinner.feedKey()) } @Test fun followListHonorsModeOverride() { val following = NotificationFeedFilter(account, TopFilter.AllFollows) - val everyone = NotificationFeedFilter(account, TopFilter.GlobalRaw) + val everyone = NotificationFeedFilter(account, TopFilter.Global) assertEquals(TopFilter.AllFollows, following.followList()) - assertEquals(TopFilter.GlobalRaw, everyone.followList()) + assertEquals(TopFilter.Global, everyone.followList()) } @Test fun followListFallsBackToSpinnerWhenOverrideNull() { val spinner = NotificationFeedFilter(account) - account.settings.defaultNotificationFollowList.value = TopFilter.Global - assertEquals(TopFilter.Global, spinner.followList()) + account.settings.defaultNotificationFollowList.value = TopFilter.Selected + assertEquals(TopFilter.Selected, spinner.followList()) account.settings.defaultNotificationFollowList.value = TopFilter.AllFollows assertEquals(TopFilter.AllFollows, spinner.followList()) @@ -139,29 +140,30 @@ class NotificationFeedFilterModeOverrideTest { @Test fun buildFilterParamsForGlobalOverrideReportsGlobal() { - val selected = NotificationFeedFilter(account, TopFilter.Global) + val everyone = NotificationFeedFilter(account, TopFilter.Global) - val params = selected.buildFilterParams(account) + val params = everyone.buildFilterParams(account) // isGlobal() short-circuits the follow-membership gate in acceptableEvent, - // which is how the Selected mode admits notifications from non-followed authors. + // which is how the Everyone tab admits notifications from non-followed authors. assertTrue( - "Selected mode's FilterByListParams must report isGlobal so non-followers pass the gate", + "Everyone tab's FilterByListParams must report isGlobal so non-followers pass the gate", params.isGlobal(), ) } @Test - fun buildFilterParamsForGlobalRawOverrideReportsGlobal() { - val everyone = NotificationFeedFilter(account, TopFilter.GlobalRaw) + fun buildFilterParamsForSelectedOverrideReportsGlobal() { + val selected = NotificationFeedFilter(account, TopFilter.Selected) - val params = everyone.buildFilterParams(account) + val params = selected.buildFilterParams(account) - // GlobalRaw rides the same GlobalFeedFlow relay set as Global, so it must - // also report isGlobal and let non-followers through; the only difference - // is that acceptableEvent skips the per-kind relevance heuristics. + // Selected rides the same GlobalFeedFlow relay set as Global, so it must + // also report isGlobal and let non-followers through; the difference is + // that acceptableEvent applies the per-kind relevance heuristics, which + // Global skips. assertTrue( - "Everyone tab's FilterByListParams must report isGlobal so non-followers pass the gate", + "Selected mode's FilterByListParams must report isGlobal so non-followers pass the gate", params.isGlobal(), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index c1b92834bf..ec033c936c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -812,7 +812,7 @@ object LocalPreferences { FollowListPrefs( home = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null), TopFilter.AllFollows), stories = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null), TopFilter.Global), - notification = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, null), TopFilter.Global), + notification = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, null), TopFilter.Selected), discovery = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null), TopFilter.Global), polls = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_POLLS_FOLLOW_LIST, null), TopFilter.Global), pictures = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_PICTURES_FOLLOW_LIST, null), TopFilter.Global), 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 64e89cc2fe..77e5810423 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -97,13 +97,14 @@ sealed class TopFilter( object Global : TopFilter(" Global ") /** - * Global without curation heuristics. Used by Notifications to show every - * event that p-tags the user, removing only hidden/reported authors and - * user-curated mutes — unlike [Global], which in Notifications (shown as - * "Selected") also applies per-kind relevance rules. + * Notifications-only curated mode: like [Global] it admits authors the + * user doesn't follow, but it also applies per-kind relevance heuristics + * to remove less interesting notes (reactions/reposts that don't target + * the user's own notes, unrelated thread replies, etc.). In Notifications, + * [Global] shows every event that p-tags the user instead. */ @Serializable - object GlobalRaw : TopFilter(" Global Raw ") + object Selected : TopFilter(" Selected ") @Serializable object AllFollows : TopFilter(" All Follows ") @@ -184,7 +185,7 @@ class AccountSettings( val hideCommunityRulesViolations: MutableStateFlow = MutableStateFlow(false), val defaultHomeFollowList: MutableStateFlow = MutableStateFlow(TopFilter.AllFollows), val defaultStoriesFollowList: MutableStateFlow = MutableStateFlow(TopFilter.Global), - val defaultNotificationFollowList: MutableStateFlow = MutableStateFlow(TopFilter.Global), + val defaultNotificationFollowList: MutableStateFlow = MutableStateFlow(TopFilter.Selected), val defaultDiscoveryFollowList: MutableStateFlow = MutableStateFlow(TopFilter.Global), val defaultPollsFollowList: MutableStateFlow = MutableStateFlow(TopFilter.Global), val defaultPicturesFollowList: MutableStateFlow = MutableStateFlow(TopFilter.Global), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt index a217c984c0..96c439dfbf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt @@ -72,7 +72,7 @@ class FeedTopNavFilterState( ) { fun loadFlowsFor(listName: TopFilter): IFeedFlowsType = when (listName) { - TopFilter.Global, TopFilter.GlobalRaw -> { + TopFilter.Global, TopFilter.Selected -> { GlobalFeedFlow(followsRelays, proxyRelays, relayFeeds) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt index 45b65cfb1b..c69233bbe1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt @@ -367,7 +367,7 @@ private fun FeedDefinition.group(): FeedGroup = when (code) { is TopFilter.AroundMe -> FeedGroup.LOCATIONS is TopFilter.Global -> FeedGroup.RELAYS - is TopFilter.GlobalRaw -> FeedGroup.RELAYS + is TopFilter.Selected -> FeedGroup.RELAYS is TopFilter.AllFavoriteAlgoFeeds -> FeedGroup.DVMS else -> FeedGroup.FEEDS } @@ -508,8 +508,8 @@ private fun FeedIcon( MaterialSymbols.Public } - is TopFilter.GlobalRaw -> { - MaterialSymbols.Public + is TopFilter.Selected -> { + MaterialSymbols.FilterAlt } is TopFilter.AroundMe -> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt index f0c79c3688..26d86eb00d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt @@ -82,20 +82,14 @@ class TopNavFilterState( name = ResourceName(R.string.follow_list_global), ) - // Notifications-only pair: the curated mode (TopFilter.Global) is shown as - // "Selected" while the raw every-p-tag mode takes the "Global" label. + // Notifications-only curated mode; in Notifications, Global itself shows + // every event that p-tags the user. val selectedFollow = FeedDefinition( - code = TopFilter.Global, + code = TopFilter.Selected, name = ResourceName(R.string.follow_list_selected), ) - val globalRawFollow = - FeedDefinition( - code = TopFilter.GlobalRaw, - name = ResourceName(R.string.follow_list_global), - ) - val aroundMe = FeedDefinition( code = TopFilter.AroundMe, @@ -122,7 +116,7 @@ class TopNavFilterState( val defaultLists = persistentListOf(allFollows, userFollows, kind3Follows, aroundMe, globalFollow, muteListFollow) - val defaultNotificationLists = persistentListOf(allFollows, userFollows, kind3Follows, aroundMe, selectedFollow, globalRawFollow, muteListFollow) + val defaultNotificationLists = persistentListOf(allFollows, userFollows, kind3Follows, aroundMe, selectedFollow, globalFollow, muteListFollow) fun mergePeopleLists( peopleLists: List, @@ -315,7 +309,7 @@ class TopNavFilterState( checkNotInMainThread() emit( listOf( - listOf(allFollows, userFollows, kind3Follows, aroundMe, selectedFollow, globalRawFollow), + listOf(allFollows, userFollows, kind3Follows, aroundMe, selectedFollow, globalFollow), peopleLists, listOf(muteListFollow), ).flatten().toImmutableList(), 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 89e9c68523..2471d0dbb1 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 @@ -127,7 +127,7 @@ class AccountFeedContentStates( val notifications = CardFeedContentState(NotificationFeedFilter(account), scope) val notificationsFollowing = CardFeedContentState(NotificationFeedFilter(account, TopFilter.AllFollows), scope) - val notificationsEveryone = CardFeedContentState(NotificationFeedFilter(account, TopFilter.GlobalRaw), scope) + val notificationsEveryone = CardFeedContentState(NotificationFeedFilter(account, TopFilter.Global), scope) val notificationsOpenPolls = OpenPollsState(account, scope) val notificationSummary = NotificationSummaryState(account) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index a27b0e2080..8036eb2e4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -328,13 +328,13 @@ class NotificationFeedFilter( // Chess events bypass the follow filter — opponents may not be followed val isChessEvent = noteEvent is LiveChessGameAcceptEvent || noteEvent is LiveChessMoveEvent - // Raw global keeps every event that p-tags the user, skipping the - // per-kind relevance heuristics that the Selected mode applies. - val isRawGlobal = followList() is TopFilter.GlobalRaw + // Global keeps every event that p-tags the user; Selected (and the + // follow/list modes) also applies the per-kind relevance heuristics. + val isRawGlobal = followList() is TopFilter.Global return noteEvent?.kind in NOTIFICATION_KINDS && (noteEvent is LnZapEvent || notifAuthor != loggedInUserHex) && - (isRawGlobal || isChessEvent || filterParams.isGlobal() || notifAuthor == null || filterParams.isAuthorInFollows(notifAuthor)) && + (isChessEvent || filterParams.isGlobal() || notifAuthor == null || filterParams.isAuthorInFollows(notifAuthor)) && noteEvent?.isTaggedUser(loggedInUserHex) ?: false && (filterParams.isHiddenList || notifAuthor == null || !account.isHidden(notifAuthor)) && (noteEvent !is PrivateDmEvent || !account.isDecryptedContentHidden(noteEvent)) && From 1ea0b91ff16cf1e0a65df38ecd9ed436649ee4de Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 17:30:02 +0000 Subject: [PATCH 3/3] feat: relabel the curated notification filter from Selected to Curated https://claude.ai/code/session_013VDWpD8Dr6sBF7tBEUZGpg --- .../com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt | 2 +- amethyst/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt index 26d86eb00d..71c615ca6c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt @@ -87,7 +87,7 @@ class TopNavFilterState( val selectedFollow = FeedDefinition( code = TopFilter.Selected, - name = ResourceName(R.string.follow_list_selected), + name = ResourceName(R.string.follow_list_curated), ) val aroundMe = diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 773149cc43..5211c191fa 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1162,7 +1162,7 @@ Follows via Proxy Around Me Global - Selected + Curated Chess Mine Mute List