/** * useSasquatchActions — publish the actions Sasquatch drafts (Pro). * ───────────────────────────────────────────────────────────────────────────── * Sasquatch never publishes on its own. The AI proposes a structured DRAFT; the * user reviews it in a confirmation card; only on confirm do we sign & publish * here. All actions reuse the app's existing event schemas. * * Supported drafts: * • post_status — NIP-38 skating status (kind 30315), optional expiry * • list_gear — NIP-99 marketplace listing (kind 30402), skateboarding-tagged */ import { useCallback } from 'react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useTranslation } from '@/contexts/language'; import { MARKETPLACE_ITEM_KIND } from '@/lib/types'; import { USER_STATUS_KIND } from '@/hooks/useUserStatus'; /** Minimal translation-function shape (this module is imported by components). */ type Translate = (key: string, params?: Record) => string; // ─── Draft shapes ───────────────────────────────────────────────────────────── export interface PostStatusDraft { type: 'post_status'; /** Freeform status text, e.g. "🛹 Skating Southbank". */ text: string; /** Minutes until it auto-expires (default 120). */ expiresInMinutes?: number; } export interface ListGearDraft { type: 'list_gear'; title: string; description?: string; /** Price in the given currency (default sats). Omit for "make offer". */ price?: number; currency?: string; /** deck | trucks | wheels | bearings | shoes | clothing | other */ category?: string; condition?: string; size?: string; } /** "Wanted" ad — the skater is LOOKING FOR gear (NIP-99 listing_type=wtb). */ export interface WtbGearDraft { type: 'wtb_gear'; /** What they're looking for. */ title: string; description?: string; /** Maximum sats they'll pay. Omit for "open". */ price?: number; currency?: string; category?: string; condition?: string; size?: string; } export type SasquatchDraft = PostStatusDraft | ListGearDraft | WtbGearDraft; // ─── Human-readable summary for the confirmation card ──────────────────────── export function summarizeDraft(d: SasquatchDraft, t: Translate): { title: string; lines: string[]; cta: string } { if (d.type === 'post_status') { const mins = d.expiresInMinutes ?? 120; return { title: t('sasquatchUi.draftPostStatusTitle'), lines: [`"${d.text}"`, t('sasquatchUi.draftAutoExpires', { count: mins })], cta: t('sasquatchUi.draftPostStatusCta'), }; } if (d.type === 'wtb_gear') { const lines: string[] = []; if (d.category) lines.push(t('sasquatchUi.draftCategory', { value: d.category })); if (d.size) lines.push(t('sasquatchUi.draftSize', { value: d.size })); if (d.condition) lines.push(t('sasquatchUi.draftCondition', { value: d.condition })); lines.push(d.price ? t('sasquatchUi.draftMaxPrice', { price: d.price.toLocaleString(), currency: d.currency ?? 'sats' }) : t('sasquatchUi.draftPriceOpen')); if (d.description) lines.push(d.description); return { title: t('sasquatchUi.draftLookingForTitle', { title: d.title }), lines, cta: t('sasquatchUi.draftWantedCta') }; } const lines: string[] = []; if (d.category) lines.push(t('sasquatchUi.draftCategory', { value: d.category })); if (d.condition) lines.push(t('sasquatchUi.draftCondition', { value: d.condition })); if (d.size) lines.push(t('sasquatchUi.draftSize', { value: d.size })); lines.push(d.price ? t('sasquatchUi.draftPrice', { price: d.price.toLocaleString(), currency: d.currency ?? 'sats' }) : t('sasquatchUi.draftPriceOffer')); if (d.description) lines.push(d.description); return { title: t('sasquatchUi.draftListTitle', { title: d.title }), lines, cta: t('sasquatchUi.draftPublishCta') }; } // ─── The hook ───────────────────────────────────────────────────────────────── export function useSasquatchActions() { const { user } = useCurrentUser(); const { mutateAsync: publishEvent } = useNostrPublish(); const { t } = useTranslation(); const publishDraft = useCallback( async (draft: SasquatchDraft): Promise<{ ok: true; label: string }> => { if (!user) throw new Error(t('sasquatchUi.mustBeLoggedIn')); if (draft.type === 'post_status') { const tags: string[][] = [['d', 'general']]; const mins = draft.expiresInMinutes ?? 120; tags.push(['expiration', String(Math.floor(Date.now() / 1000) + mins * 60)]); await publishEvent({ kind: USER_STATUS_KIND, content: draft.text.slice(0, 140), tags }); return { ok: true, label: t('sasquatchUi.statusPosted') }; } if (draft.type === 'wtb_gear') { const now = Math.floor(Date.now() / 1000); const identifier = `wtb-${now}-${Math.random().toString(36).slice(2, 8)}`; const currency = draft.currency ?? 'sats'; const tags: string[][] = [ ['d', identifier], ['title', draft.title.slice(0, 120)], ['t', 'skateboarding'], ['listing_type', 'wtb'], ['status', 'active'], ['published_at', String(now)], ['alt', `Wanted: ${draft.title}`], ]; if (draft.category) tags.push(['t', draft.category]); if (typeof draft.price === 'number' && draft.price > 0) { tags.push(['price_type', 'fixed']); tags.push(['price', String(draft.price), currency]); } else { tags.push(['price_type', 'offer']); } if (draft.condition) tags.push(['condition', draft.condition]); if (draft.size) tags.push(['size', draft.size]); await publishEvent({ kind: MARKETPLACE_ITEM_KIND, content: (draft.description ?? '').slice(0, 2000), tags, }); return { ok: true, label: t('sasquatchUi.wantedPublished') }; } // list_gear const now = Math.floor(Date.now() / 1000); const identifier = `gear-${now}-${Math.random().toString(36).slice(2, 8)}`; const currency = draft.currency ?? 'sats'; const tags: string[][] = [ ['d', identifier], ['title', draft.title.slice(0, 120)], ['t', 'skateboarding'], ['listing_type', 'sell'], ['status', 'active'], ['published_at', String(now)], ['alt', `Gear listing: ${draft.title}`], ]; if (draft.category) tags.push(['t', draft.category]); if (typeof draft.price === 'number' && draft.price > 0) { tags.push(['price_type', 'fixed']); tags.push(['price', String(draft.price), currency]); } else { tags.push(['price_type', 'offer']); } if (draft.condition) tags.push(['condition', draft.condition]); if (draft.size) tags.push(['size', draft.size]); await publishEvent({ kind: MARKETPLACE_ITEM_KIND, content: (draft.description ?? '').slice(0, 2000), tags, }); return { ok: true, label: t('sasquatchUi.listingPublished') }; }, [user, publishEvent, t], ); return { publishDraft }; }