fix(notifications): remove the 7-day since floor that emptied the tab

The notifications query pinned `since` to `oneWeekAgo()` whenever it had no
EOSE timestamp. Combined with two other facts that produced a deadlock:

- the EOSE `since` map is in-memory only (`SincePerRelayMap = MutableMap<..>`),
  so EVERY cold start re-pinned the window to 7 days; and
- the backward-paging escape hatch (`lastNoteCreatedAtIfFilled()`) only arms
  once the feed holds a FULL page (`notes.size >= localFilter.limit()`).

The feed could not fill a page because the query only asked for a week, and the
query could not widen because the feed never filled. Any account whose last
inbound mention was older than 7 days saw a near-empty Notifications tab
forever — including a fresh install of a long-established account.

Worse, the floor was applied to `filterSummaryNotificationsToPubkey` (kinds
1/7/6/9735 — the overwhelming bulk of notifications), which was not even wired
to the paging fallback that the secondary per-key kinds received.

Home is the precedent and does not do this: `filterHomePostsByAllFollows`
passes `since ?: boundary`, i.e. plain null on a cold start, relying on the
relay-side `limit` to bound the response. Notifications was the outlier. These
filters are `#p`-scoped to the user's own key and carry a `limit`
(2000/500/200/20), so an all-time query is one index scan returning at most
`limit` events newest-first — same cost, strictly more useful.

Measured on the test account before the fix: 163 events p-tagging it exist on
relays and are fetchable (100 kind-1 and 100 kind-7, both hitting the query
cap), 77 of which clear the follow gate — yet the tab rendered ~3, because the
account's most recent inbound activity was a month old. After the fix the tab
scrolls back through Feb 2025.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-07-20 11:34:07 -04:00
co-authored by Claude Opus 4.8
parent 4ee0416a90
commit 97b861dd5e
2 changed files with 28 additions and 7 deletions
@@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
@@ -51,17 +50,35 @@ class AccountNotificationsEoseFromInboxRelaysManager(
key: AccountQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
// Backward-paging boundary: once the feed has filled a page, ask for everything older than
// its oldest card. It stays null until then — see the note on the missing week floor below,
// which is what let it stay null forever on a quiet inbox.
val pagingBoundary = key.feedContentStates.notifications.lastNoteCreatedAtIfFilled()
val inbox =
key.account.notificationRelays.flow.value.flatMap {
// No `since` floor on the first fetch. These filters are scoped by `#p` to my own
// key and carry a relay-side `limit`, so an all-time query costs one index scan and
// returns at most `limit` events, newest first — exactly what Home does (it passes
// `since ?: boundary`, i.e. null on a cold start).
//
// This used to fall back to `oneWeekAgo()`, which silently emptied the tab for
// anyone whose last mention was older than a week: EOSE `since` is in-memory only,
// so EVERY cold start re-pinned the window to 7 days, and the paging boundary above
// could never rescue it — it only arms once the feed holds a full page, and the feed
// could not fill because the query only ever asked for a week. A fresh install of an
// established account hit the same deadlock.
val notificationSince = since?.get(it)?.time ?: pagingBoundary
filterSummaryNotificationsToPubkey(
relay = it,
pubkey = user(key).pubkeyHex,
since = since?.get(it)?.time ?: TimeUtils.oneWeekAgo(),
since = notificationSince,
) +
filterNotificationsToPubkey(
relay = it,
pubkey = user(key).pubkeyHex,
since = since?.get(it)?.time ?: key.feedContentStates.notifications.lastNoteCreatedAtIfFilled() ?: TimeUtils.oneWeekAgo(),
since = notificationSince,
)
}
@@ -76,7 +93,9 @@ class AccountNotificationsEoseFromInboxRelaysManager(
relay = relay,
pubkey = user(key).pubkeyHex,
groupIds = groupIds.distinct(),
since = since?.get(relay)?.time ?: TimeUtils.oneWeekAgo(),
// Same reasoning as the inbox filters above: `#p` + `#h` + `limit = 200`
// already bound this, so a week floor only hides older group activity.
since = since?.get(relay)?.time ?: pagingBoundary,
)
}
@@ -27,7 +27,6 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
@@ -51,8 +50,11 @@ class AccountNotificationsEoseFromRandomRelaysManager(
key: AccountQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
// only loads this after the feed is built
val defaultSince = key.feedContentStates.notifications.lastNoteCreatedAtIfFilled() ?: TimeUtils.oneWeekAgo()
// only loads this after the feed is built, so it stays null on a quiet inbox. No week floor
// behind it: this probe is `#p`-scoped to me with `limit = 20`, so relays answer with the 20
// newest either way — the floor only ever hid notifications older than a week, and since the
// boundary above needs a full page to arm, a quiet inbox could never page past it.
val defaultSince = key.feedContentStates.notifications.lastNoteCreatedAtIfFilled()
return (key.account.followsPerRelay.value.keys - key.account.notificationRelays.flow.value).flatMap {
val since = since?.get(it)?.time ?: defaultSince
filterJustTheLatestNotificationsToPubkeyFromRandomRelays(it, user(key).pubkeyHex, since)