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).
55 lines
2.3 KiB
TypeScript
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)" },
|
|
];
|