mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 00:37:41 +00:00
refactor: decouple Android NOTIFICATION_KINDS from desktop's subscription list
Android never uses NotificationKinds.SUBSCRIPTION_KINDS for relay subscriptions — every kind in it already arrives via Android's own datasources (FilterNotificationsToPubkey, the chat datasources, the wallet assembler) or via local unwrapping (14/15). Spreading it into NOTIFICATION_KINDS only coupled Android's display gate to a list whose job is desktop's relay-filter/toast allow-list, which is exactly how the kind-1059 wrap leaked into the Android notifications tab. Restore NOTIFICATION_KINDS as an explicit flat set (identical content to the previous commit's subtraction) and add NotificationKindsContractTest as the drift tripwire: envelope kinds (1059/21059) must never render on the Android tab, and every kind desktop notifies on must be either displayable on Android or a known envelope. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011A6iSrJJHUoz686eDnCQMV
This commit is contained in:
+43
-37
@@ -64,7 +64,7 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEven
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent
|
||||
import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent
|
||||
import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent
|
||||
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
||||
@@ -80,6 +80,7 @@ import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent
|
||||
import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent
|
||||
import com.vitorpamplona.quartz.nipF4Podcasts.episode.PodcastEpisodeEvent
|
||||
import com.vitorpamplona.quartz.nipF4Podcasts.metadata.PodcastMetadataEvent
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -120,42 +121,47 @@ class NotificationFeedFilter(
|
||||
)
|
||||
|
||||
val NOTIFICATION_KINDS =
|
||||
// The core subscription kinds are shared with Desktop through
|
||||
// `commons/.../moderation/notifications/NotificationKinds.SUBSCRIPTION_KINDS`
|
||||
// so a change on either platform automatically propagates.
|
||||
// Android-only extras (badge awards, git issues/patches/PRs,
|
||||
// highlights, polls, videos, voice, public messages,
|
||||
// live-activities chat) stay here because Desktop has no
|
||||
// rendering for those kinds today.
|
||||
//
|
||||
// GiftWrap (1059) is subscription-only: the wrap is an envelope whose
|
||||
// created_at is randomized up to 2 days back (NIP-59), so rendering it
|
||||
// as a feed row would misorder the tab with undecryptable entries. On
|
||||
// Android the unwrapped inner event (kind 14/15/…) is what notifies —
|
||||
// same rule NotificationDispatcher applies to push. Desktop keeps the
|
||||
// wrap in its inbox because it has no unwrap pipeline there yet.
|
||||
(
|
||||
com.vitorpamplona.amethyst.commons.moderation.notifications.NotificationKinds
|
||||
.SUBSCRIPTION_KINDS
|
||||
.toSet() - GiftWrapEvent.KIND
|
||||
) +
|
||||
setOf(
|
||||
BadgeAwardEvent.KIND,
|
||||
GitIssueEvent.KIND,
|
||||
GitPatchEvent.KIND,
|
||||
GitPullRequestEvent.KIND,
|
||||
GitPullRequestUpdateEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
LiveActivitiesChatMessageEvent.KIND,
|
||||
PictureEvent.KIND,
|
||||
PollEvent.KIND,
|
||||
ZapPollEvent.KIND,
|
||||
PublicMessageEvent.KIND,
|
||||
VideoNormalEvent.KIND,
|
||||
VideoShortEvent.KIND,
|
||||
VoiceEvent.KIND,
|
||||
VoiceReplyEvent.KIND,
|
||||
) + ADDRESSABLE_KINDS
|
||||
// Kinds that RENDER as a row on the Notifications tab. This is a
|
||||
// display gate over whatever is already in LocalCache — it plays no
|
||||
// part in relay subscriptions (those live in
|
||||
// FilterNotificationsToPubkey, the chat datasources, and the wallet
|
||||
// assembler). It deliberately does NOT share Desktop's
|
||||
// `NotificationKinds.SUBSCRIPTION_KINDS`: that list answers "what to
|
||||
// ask relays for / toast on" and includes envelope kinds like
|
||||
// GiftWrap (1059), whose created_at is randomized up to 2 days back
|
||||
// (NIP-59). Envelopes never render here — the unwrapped inner event
|
||||
// (kind 14/15/…) is the feed row, same rule NotificationDispatcher
|
||||
// applies to push. NotificationKindsContractTest pins the
|
||||
// relationship between the two lists.
|
||||
setOf(
|
||||
BadgeAwardEvent.KIND,
|
||||
ChannelMessageEvent.KIND,
|
||||
ChatMessageEvent.KIND,
|
||||
ChatMessageEncryptedFileHeaderEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
GenericRepostEvent.KIND,
|
||||
GitIssueEvent.KIND,
|
||||
GitPatchEvent.KIND,
|
||||
GitPullRequestEvent.KIND,
|
||||
GitPullRequestUpdateEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
TextNoteEvent.KIND,
|
||||
ReactionEvent.KIND,
|
||||
RepostEvent.KIND,
|
||||
LnZapEvent.KIND,
|
||||
NutzapEvent.KIND,
|
||||
OnchainZapEvent.KIND,
|
||||
LiveActivitiesChatMessageEvent.KIND,
|
||||
PictureEvent.KIND,
|
||||
PollEvent.KIND,
|
||||
ZapPollEvent.KIND,
|
||||
PrivateDmEvent.KIND,
|
||||
PublicMessageEvent.KIND,
|
||||
VideoNormalEvent.KIND,
|
||||
VideoShortEvent.KIND,
|
||||
VoiceEvent.KIND,
|
||||
VoiceReplyEvent.KIND,
|
||||
) + ADDRESSABLE_KINDS
|
||||
|
||||
// How deep to walk a public chat reply chain looking for one of the
|
||||
// user's own messages. Bounds the cost on very long threads; the
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.moderation.notifications.NotificationKinds
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Pins the relationship between the two independently-maintained kind lists:
|
||||
*
|
||||
* - `NotificationKinds.SUBSCRIPTION_KINDS` (commons) — what Desktop asks
|
||||
* relays for and toasts on. May contain ENVELOPE kinds (gift wraps) whose
|
||||
* created_at is randomized per NIP-59, because Desktop surfaces the wrap
|
||||
* itself as a DM row.
|
||||
* - `NotificationFeedFilter.NOTIFICATION_KINDS` (Android) — what renders as
|
||||
* a row on the Notifications tab. Envelopes must never appear here; the
|
||||
* unwrapped inner event is the row.
|
||||
*
|
||||
* The lists were briefly coupled (NOTIFICATION_KINDS spread
|
||||
* SUBSCRIPTION_KINDS), which silently pulled the kind-1059 wrap into the
|
||||
* Android feed. They are now maintained separately; this test is the tripwire
|
||||
* that keeps them from drifting apart unintentionally in either direction.
|
||||
*/
|
||||
class NotificationKindsContractTest {
|
||||
private val envelopeKinds = setOf(GiftWrapEvent.KIND, EphemeralGiftWrapEvent.KIND)
|
||||
|
||||
@Test
|
||||
fun `envelope kinds never render on the Android notifications tab`() {
|
||||
val leaked = envelopeKinds.intersect(NotificationFeedFilter.NOTIFICATION_KINDS.toSet())
|
||||
assertTrue(
|
||||
"Envelope kinds $leaked are in NOTIFICATION_KINDS. Wraps have a " +
|
||||
"randomized created_at (NIP-59) and no decryptable payload to render — " +
|
||||
"the unwrapped inner event is the feed row. If a new envelope kind is " +
|
||||
"intentional, unwrap it instead of displaying it.",
|
||||
leaked.isEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `every kind desktop notifies on is displayable on Android or a known envelope`() {
|
||||
val unaccounted =
|
||||
NotificationKinds.SUBSCRIPTION_KINDS.toSet() -
|
||||
NotificationFeedFilter.NOTIFICATION_KINDS.toSet() -
|
||||
envelopeKinds
|
||||
|
||||
assertTrue(
|
||||
"Kinds $unaccounted were added to the shared SUBSCRIPTION_KINDS but are " +
|
||||
"neither displayable on the Android notifications tab nor a known " +
|
||||
"envelope kind. Either add them to NOTIFICATION_KINDS (if Android " +
|
||||
"should render them) or to envelopeKinds in this test (if they only " +
|
||||
"deliver an inner payload).",
|
||||
unaccounted.isEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user