import { useMemo } from 'react'; import { useOfferThreadsAddressedToMe, type MarketplaceOffer } from '@/hooks/useMarketplaceOffers'; import { useIncomingTradeRequests, usePendingTradeConfirmations } from '@/hooks/useTradeAttestations'; /** * Trade/offer activity for the logged-in user — the buying/selling steps that * deserve a nudge: a new offer on your listing, a counter-offer it's your turn * to answer, the outcome of YOUR offer (accepted / declined / withdrawn), a * request to confirm a completed trade, and a counterparty CONFIRMING the trade * (which is what turns it into mutual, reputation-building proof). * * All of it is derived from events already addressed to the user, so it needs no * extra infrastructure — the same events power the bell and the OS notifications. */ export type TradeNotificationType = | 'offer' | 'counter' | 'accepted' | 'declined' | 'withdrawn' | 'trade-request' | 'trade-confirm'; export interface TradeNotification { id: string; type: TradeNotificationType; createdAt: number; /** The other party (buyer for an offer, the requester for a request). */ counterparty: string; amount?: number; currency?: string; /** In-kind offer (a swap/barter), when the sender offered an item. */ offerItem?: string; listingPubkey: string; listingIdentifier: string; /** Present for offer/counter items, so the UI can act inline. */ offer?: MarketplaceOffer; } export function useTradeNotifications(pubkey: string | undefined) { const { data: incoming = [], isLoading: offersLoading } = useOfferThreadsAddressedToMe(pubkey); const { data: requests = [], isLoading: requestsLoading } = useIncomingTradeRequests(pubkey); const { data: pendingConfirms = [], isLoading: confirmsLoading } = usePendingTradeConfirmations(pubkey); const notifications = useMemo(() => { const out: TradeNotification[] = []; // Every step addressed to me: a new offer, a counter it's my turn to answer, // or the outcome of my own offer (accepted / declined / withdrawn). for (const thread of incoming) { const latest = thread.latest; const type: TradeNotificationType = latest.status === 'open' ? latest.replyTo ? 'counter' : 'offer' : latest.status; out.push({ id: latest.id, type, createdAt: latest.createdAt, counterparty: latest.author, amount: latest.amount, currency: latest.currency, offerItem: latest.offerItem, listingPubkey: latest.listingPubkey, listingIdentifier: latest.listingIdentifier, offer: latest, }); } // "Please confirm our trade" requests (buyer → seller). for (const r of requests) { out.push({ id: r.event.id, type: 'trade-request', createdAt: r.event.created_at, counterparty: r.requestor, listingPubkey: r.listing.pubkey, listingIdentifier: r.listing.identifier, }); } // A counterparty has recorded/confirmed the deal and you haven't mirrored it // yet — confirming back is what makes it a mutual, reputation-building trade. for (const p of pendingConfirms) { out.push({ id: `confirm:${p.eventId}`, type: 'trade-confirm', createdAt: p.createdAt, counterparty: p.seller, listingPubkey: p.listing.pubkey, listingIdentifier: p.listing.identifier, }); } return out.sort((a, b) => b.createdAt - a.createdAt); }, [incoming, requests, pendingConfirms]); return { notifications, isLoading: offersLoading || requestsLoading || confirmsLoading }; } /** "2,000 SATS + an 8.25 deck" — sats, an item, or both. */ function offerWhat(n: TradeNotification): string { const parts: string[] = []; if (n.amount) parts.push(`${n.amount.toLocaleString()} ${n.currency ?? 'SATS'}`); if (n.offerItem) parts.push(n.offerItem); return parts.join(' + '); } export function tradeNotificationText(n: TradeNotification): string { const what = offerWhat(n); switch (n.type) { case 'offer': return what ? `offered ${what}` : 'made an offer'; case 'counter': return what ? `countered with ${what}` : 'sent a counter-offer'; case 'accepted': return n.amount ? `accepted your offer — ${n.amount.toLocaleString()} ${n.currency ?? 'SATS'}` : 'accepted your offer'; case 'declined': return 'declined your offer'; case 'withdrawn': return 'withdrew their offer'; case 'trade-request': return 'asked you to confirm a trade'; case 'trade-confirm': return 'confirmed your trade — confirm back to build both reputations'; default: return 'needs your attention'; } }