Merge pull request #3196 from vitorpamplona/claude/nice-ptolemy-9153uk
Add Selected filter mode for curated notifications
This commit is contained in:
+22
-5
@@ -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.Global` (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 {
|
||||
@@ -130,8 +131,8 @@ class NotificationFeedFilterModeOverrideTest {
|
||||
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())
|
||||
@@ -151,6 +152,22 @@ class NotificationFeedFilterModeOverrideTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildFilterParamsForSelectedOverrideReportsGlobal() {
|
||||
val selected = NotificationFeedFilter(account, TopFilter.Selected)
|
||||
|
||||
val params = selected.buildFilterParams(account)
|
||||
|
||||
// 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(
|
||||
"Selected mode's FilterByListParams must report isGlobal so non-followers pass the gate",
|
||||
params.isGlobal(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildFilterParamsForAllFollowsOverrideIsNotGlobal() {
|
||||
val following = NotificationFeedFilter(account, TopFilter.AllFollows)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -96,6 +96,16 @@ sealed class TopFilter(
|
||||
@Serializable
|
||||
object Global : TopFilter(" Global ")
|
||||
|
||||
/**
|
||||
* 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 Selected : TopFilter(" Selected ")
|
||||
|
||||
@Serializable
|
||||
object AllFollows : TopFilter(" All Follows ")
|
||||
|
||||
@@ -175,7 +185,7 @@ class AccountSettings(
|
||||
val hideCommunityRulesViolations: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
val defaultHomeFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.AllFollows),
|
||||
val defaultStoriesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultNotificationFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultNotificationFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Selected),
|
||||
val defaultDiscoveryFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultPollsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
val defaultPicturesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ class FeedTopNavFilterState(
|
||||
) {
|
||||
fun loadFlowsFor(listName: TopFilter): IFeedFlowsType =
|
||||
when (listName) {
|
||||
TopFilter.Global -> {
|
||||
TopFilter.Global, TopFilter.Selected -> {
|
||||
GlobalFeedFlow(followsRelays, proxyRelays, relayFeeds)
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -367,6 +367,7 @@ private fun FeedDefinition.group(): FeedGroup =
|
||||
when (code) {
|
||||
is TopFilter.AroundMe -> FeedGroup.LOCATIONS
|
||||
is TopFilter.Global -> FeedGroup.RELAYS
|
||||
is TopFilter.Selected -> FeedGroup.RELAYS
|
||||
is TopFilter.AllFavoriteAlgoFeeds -> FeedGroup.DVMS
|
||||
else -> FeedGroup.FEEDS
|
||||
}
|
||||
@@ -507,6 +508,10 @@ private fun FeedIcon(
|
||||
MaterialSymbols.Public
|
||||
}
|
||||
|
||||
is TopFilter.Selected -> {
|
||||
MaterialSymbols.FilterAlt
|
||||
}
|
||||
|
||||
is TopFilter.AroundMe -> {
|
||||
MaterialSymbols.LocationOn
|
||||
}
|
||||
|
||||
@@ -82,6 +82,14 @@ class TopNavFilterState(
|
||||
name = ResourceName(R.string.follow_list_global),
|
||||
)
|
||||
|
||||
// Notifications-only curated mode; in Notifications, Global itself shows
|
||||
// every event that p-tags the user.
|
||||
val selectedFollow =
|
||||
FeedDefinition(
|
||||
code = TopFilter.Selected,
|
||||
name = ResourceName(R.string.follow_list_curated),
|
||||
)
|
||||
|
||||
val aroundMe =
|
||||
FeedDefinition(
|
||||
code = TopFilter.AroundMe,
|
||||
@@ -108,6 +116,8 @@ class TopNavFilterState(
|
||||
|
||||
val defaultLists = persistentListOf(allFollows, userFollows, kind3Follows, aroundMe, globalFollow, muteListFollow)
|
||||
|
||||
val defaultNotificationLists = persistentListOf(allFollows, userFollows, kind3Follows, aroundMe, selectedFollow, globalFollow, muteListFollow)
|
||||
|
||||
fun mergePeopleLists(
|
||||
peopleLists: List<AddressableNote>,
|
||||
followLists: List<AddressableNote>,
|
||||
@@ -294,6 +304,18 @@ class TopNavFilterState(
|
||||
)
|
||||
}
|
||||
|
||||
private val _notificationLists =
|
||||
livePeopleListsFlow.transform { peopleLists ->
|
||||
checkNotInMainThread()
|
||||
emit(
|
||||
listOf(
|
||||
listOf(allFollows, userFollows, kind3Follows, aroundMe, selectedFollow, globalFollow),
|
||||
peopleLists,
|
||||
listOf(muteListFollow),
|
||||
).flatten().toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
val kind3GlobalPeopleRoutes =
|
||||
_kind3GlobalPeopleRoutes
|
||||
.flowOn(Dispatchers.IO)
|
||||
@@ -304,6 +326,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)
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+5
-1
@@ -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
|
||||
|
||||
// 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) &&
|
||||
(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<Note>): List<Note> = items.sortedWith(DefaultFeedOrder)
|
||||
|
||||
@@ -1180,6 +1180,7 @@
|
||||
<string name="follow_list_kind3follows_proxy">Follows via Proxy</string>
|
||||
<string name="follow_list_aroundme">Around Me</string>
|
||||
<string name="follow_list_global">Global</string>
|
||||
<string name="follow_list_curated">Curated</string>
|
||||
<string name="follow_list_chess">Chess</string>
|
||||
<string name="follow_list_mine">Mine</string>
|
||||
<string name="follow_list_mute_list">Mute List</string>
|
||||
|
||||
Reference in New Issue
Block a user