From 9155949fda7b5dd335b1829f7d1ca17ed95e0d79 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Thu, 1 Oct 2026 20:20:10 +0700 Subject: [PATCH] feat(push): extend categories to crew + social; master switch tears down push - CATEGORY_BY_KIND now maps crew invite (36229) / join-request (31969) -> crew and zap (9735) / reply-mention (1) / comment (1111) -> social; the relay watch includes those kinds. Reactions/reposts are deliberately not pushed (noisy) and newsletter has no push channel. - Master switch off now also disables web push and the native poller. - Verified: a crew-only token is not woken by a zap but is by a crew invite. --- push-service/server.ts | 48 ++++++++++++++++++--- src/components/NotificationSettingsCard.tsx | 3 ++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/push-service/server.ts b/push-service/server.ts index 60d0107..9c13955 100644 --- a/push-service/server.ts +++ b/push-service/server.ts @@ -67,8 +67,9 @@ interface StoredSubscription { } /** - * Event kind → notification category, mirroring the app's `NotificationCategory` - * (only `dms` and `trades` are pushed — social/crew/newsletter are in-app only). + * Event kind → notification category, mirroring the app's `NotificationCategory`. + * `newsletter` has no push channel (readers poll it); reactions/reposts are + * deliberately excluded (too noisy). */ const CATEGORY_BY_KIND: Record = { 1059: 'dms', // NIP-17 gift wrap @@ -76,6 +77,19 @@ const CATEGORY_BY_KIND: Record = { 31871: 'trades', // trade attestation (sold / bought) 31872: 'trades', // confirm-request 34967: 'trades', // shipment status + 36229: 'crew', // crew invitation + 31969: 'crew', // crew join request + 9735: 'social', // zap receipt + 1: 'social', // reply / mention + 1111: 'social', // comment +}; + +/** Where a tap should land, per category. */ +const URL_BY_CATEGORY: Record = { + trades: '/#/trades', + crew: '/#/crews', + social: '/#/', + dms: '/#/messages', }; function allows(categories: string[] | undefined, category: string): boolean { @@ -197,7 +211,22 @@ const OFFER_KIND = 30403; // offer / counter / accepted / declined / wit const TRADE_CONFIRM_KIND = 31871; // counterparty recorded the trade const TRADE_REQUEST_KIND = 31872; // "please confirm our trade" const SHIPMENT_STATUS_KIND = 34967; // shipped / received / issue (self-reported) -const MARKET_KINDS = [OFFER_KIND, TRADE_CONFIRM_KIND, TRADE_REQUEST_KIND, SHIPMENT_STATUS_KIND]; +const CREW_INVITE_KIND = 36229; // crew invitation +const CREW_JOIN_REQUEST_KIND = 31969; // crew join request +const ZAP_RECEIPT_KIND = 9735; // zap +const TEXT_NOTE_KIND = 1; // reply / mention +const COMMENT_KIND = 1111; // comment +const MARKET_KINDS = [ + OFFER_KIND, + TRADE_CONFIRM_KIND, + TRADE_REQUEST_KIND, + SHIPMENT_STATUS_KIND, + CREW_INVITE_KIND, + CREW_JOIN_REQUEST_KIND, + ZAP_RECEIPT_KIND, + TEXT_NOTE_KIND, + COMMENT_KIND, +]; function subscribedPubkeys(): string[] { // Web subscribers AND native poll-token holders — we watch for both. @@ -208,6 +237,10 @@ function subscribedPubkeys(): string[] { /** Content-free body — no amounts, no item text (that stays out of the push). */ function marketBody(ev: NostrEvent): string { + if (ev.kind === CREW_INVITE_KIND) return 'invited you to a crew'; + if (ev.kind === CREW_JOIN_REQUEST_KIND) return 'asked to join your crew'; + if (ev.kind === ZAP_RECEIPT_KIND) return 'You got a zap ⚡'; + if (ev.kind === TEXT_NOTE_KIND || ev.kind === COMMENT_KIND) return 'New reply or mention'; if (ev.kind === TRADE_REQUEST_KIND) return 'asked you to confirm a trade'; if (ev.kind === TRADE_CONFIRM_KIND) return 'confirmed your trade'; if (ev.kind === SHIPMENT_STATUS_KIND) { @@ -274,11 +307,16 @@ function handleEvent(ev: NostrEvent): void { // 2) Marketplace events addressed to a subscriber (public — metadata only; // we only ever send a generic body, never the amount or item). if (MARKET_KINDS.includes(ev.kind)) { + const category = CATEGORY_BY_KIND[ev.kind] ?? 'trades'; const recipients = [...new Set(ev.tags.filter(([n]) => n === 'p').map(([, v]) => v).filter(Boolean))]; for (const r of recipients) { if (r === ev.pubkey) continue; // never notify the author about their own message - console.log(`[push] 🔔 market ${ev.kind} ${ev.id.slice(0, 12)}… → ${r.slice(0, 12)}…`); - void pushTo(r, { title: 'SkateSpots', body: marketBody(ev), wrapId: ev.id, url: '/#/trades' }, 'trades'); + console.log(`[push] 🔔 ${category} ${ev.kind} ${ev.id.slice(0, 12)}… → ${r.slice(0, 12)}…`); + void pushTo( + r, + { title: 'SkateSpots', body: marketBody(ev), wrapId: ev.id, url: URL_BY_CATEGORY[category] ?? '/#/' }, + category, + ); } } } diff --git a/src/components/NotificationSettingsCard.tsx b/src/components/NotificationSettingsCard.tsx index cffb93b..59d73ea 100644 --- a/src/components/NotificationSettingsCard.tsx +++ b/src/components/NotificationSettingsCard.tsx @@ -70,6 +70,9 @@ export function NotificationSettingsCard() { const toggleEnabled = (next: boolean) => { if (!next) { setPrefs((prev) => ({ ...prev, enabled: false })); + // One kill switch: turning the master off also stops push + the native poller. + void push.disable(); + void background.disable(); return; } if (permission === 'granted') {