SkateSpots/src/lib/notificationPrefs.ts
SkateSpots a6aeb2feb0 feat(market): counter-offer negotiations + trade notifications
Offers were one-way (buyer proposes, seller accepts/declines) and fixed-slot
addressable events, so the buyer could never counter back and the haggling left
no trace. Now it's a thread.

- useMarketplaceOffers: every message gets a UNIQUE d (append-only) with a
  'role' tag (offer = buyer / response = seller), 'e' = the message it answers,
  and status. New counterOffer() plus respondToOffer(previous, status). Parsing
  still accepts the legacy fixed-slot d form when the role tag is absent.
- useListingOffers returns OfferThread[] (oldest first) with  = whose
  turn it is; useIncomingOffers returns the threads awaiting me (across listings).
- ListingOffersPanel: shows the volley, and accept/decline/counter (inline amount)
  for whichever side's turn it is, with withdraw on your own open offer. Counter
  notifications are nudged over DM too.
- New useTradeNotifications: incoming offers, counters and 'confirm our trade'
  requests, derived from events already addressed to the user.
- Bell: trade items render first in All and count toward the badge (vs seenAt).
- OS notifications: new 'trades' category in notificationPrefs + a trades source
  in OsNotificationBridge.
- NIP.md updated for the thread model (legacy form documented).
2026-09-29 18:33:11 +07:00

55 lines
2.3 KiB
TypeScript

/**
* On-device notification preferences.
*
* These are deliberately **device-local** (localStorage, never synced): whether
* YOUR phone busses is a property of that phone, not of your Nostr identity, and
* different devices legitimately want different settings.
*
* Notifications are strictly **opt-in** — `enabled` defaults to `false` and
* nothing is requested from the OS until the user turns them on in Settings.
*/
export type NotificationCategory = 'dms' | 'crew' | 'social' | 'newsletter' | 'reminders' | 'trades';
export interface NotificationPrefs {
/** Master switch. Off until the user explicitly enables notifications. */
enabled: boolean;
/** Direct messages (derived from the encrypted gift-wrap ENVELOPE — never decrypted to notify). */
dms: boolean;
/** Crew invites, join requests and membership confirmations. */
crew: boolean;
/** Reactions, reposts, zaps, mentions, replies, RSVPs and event updates. */
social: boolean;
/** New SkateSpots newsletter drops (subscribers only). */
newsletter: boolean;
/** Marketplace offers, counter-offers and trade confirmations. */
trades: boolean;
/** Scheduled event reminders (Android app — fires even when the app is closed). */
reminders: boolean;
}
export const NOTIFICATION_PREFS_KEY = 'skatespots:notifications:prefs';
export const DEFAULT_NOTIFICATION_PREFS: NotificationPrefs = {
enabled: false,
dms: true,
crew: true,
social: true,
newsletter: true,
trades: true,
reminders: true,
};
/** Display metadata for the per-category toggles, in order. */
export const NOTIFICATION_CATEGORIES: Array<{
key: NotificationCategory;
label: string;
hint: string;
}> = [
{ key: 'dms', label: 'Direct messages', hint: 'Someone sends you a private message' },
{ key: 'crew', label: 'Crews', hint: 'Invites, join requests and membership confirmations' },
{ key: 'social', label: 'Zaps, replies & mentions', hint: 'Reactions, reposts, zaps, replies and event updates' },
{ key: 'newsletter', label: 'Newsletter', hint: 'New SkateSpots drops (subscribers only)' },
{ key: 'trades', label: 'Offers & trades', hint: 'Offers, counter-offers and trade confirmations' },
{ key: 'reminders', label: 'Event reminders', hint: "A nudge an hour before events you're going to (Android app)" },
];