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.
This commit is contained in:
SkateSpots 2026-10-01 20:20:10 +07:00
parent 4df961b015
commit 9155949fda
2 changed files with 46 additions and 5 deletions

View file

@ -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<number, string> = {
1059: 'dms', // NIP-17 gift wrap
@ -76,6 +77,19 @@ const CATEGORY_BY_KIND: Record<number, string> = {
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<string, string> = {
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,
);
}
}
}

View file

@ -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') {