diff --git a/NIP.md b/NIP.md index 2a99d11..8047c4a 100644 --- a/NIP.md +++ b/NIP.md @@ -1168,9 +1168,9 @@ When decrypted, the content contains a JSON object with the user's inventory: |-------|-------------| | `private` | Only visible to the owner. Encrypted in the kind 30001 blob. | | `friends` | Reserved for future friends-only sharing (currently treated as private). | -| `public` | Published as a separate public event (see below) and shown in the owner's profile "Quiver". | +| `public` | Published as a separate public event (see below) and shown in the owner's public Stash on their profile. | -### Public Showcase ("The Quiver") +### Public Stash Showcase Because the Stash (kind 30001) is a single NIP-44 encrypted blob, its contents are never readable by others. To make an item genuinely public, the client @@ -1184,8 +1184,8 @@ Two flavors (hybrid model): | Case | Marker tags | Where it appears | |------|-------------|------------------| -| Public, **not** for sale | `["showcase", "true"]`, `["price", "0", "SATS"]` | Profile Quiver only (filtered out of the sellable marketplace) | -| Public **and** for sale/trade | `["price", "", "SATS"]`, `["listing_type", "selling"]`, `["status", "active"]`, optional `["trade", "true"]` | Profile Quiver (badged "For Sale") **and** the marketplace | +| Public, **not** for sale | `["showcase", "true"]`, `["price", "0", "SATS"]` | Public Stash on the profile only (filtered out of the sellable marketplace) | +| Public **and** for sale/trade | `["price", "", "SATS"]`, `["listing_type", "selling"]`, `["status", "active"]`, optional `["trade", "true"]` | Public Stash on the profile (badged "For Sale") **and** the marketplace | Clients rendering the marketplace MUST skip events carrying `["showcase", "true"]` so showcase-only items don't pollute buy/sell listings. diff --git a/src/hooks/useMarketplace.ts b/src/hooks/useMarketplace.ts index b4f4482..5f5829d 100644 --- a/src/hooks/useMarketplace.ts +++ b/src/hooks/useMarketplace.ts @@ -31,7 +31,7 @@ export function useMarketplaceItems(limit = 100) { const items: MarketplaceItem[] = []; for (const event of events) { // Skip showcase-only Stash items (public gear that is NOT for sale): - // they belong on the profile Quiver, not the sellable marketplace. + // they belong on the profile's public Stash, not the sellable marketplace. const isShowcaseOnly = event.tags.some((t) => t[0] === 'showcase' && t[1] === 'true'); if (isShowcaseOnly) continue; const item = parseMarketplaceItem(event); diff --git a/src/hooks/useStashShowcase.ts b/src/hooks/useStashShowcase.ts index b07edad..50b3fe8 100644 --- a/src/hooks/useStashShowcase.ts +++ b/src/hooks/useStashShowcase.ts @@ -1,20 +1,20 @@ /** * useStashShowcase * ───────────────────────────────────────────────────────────────────────────── - * Public visibility for Stash items ("The Quiver"). + * Public visibility for Stash items (the public Stash shown on a profile). * * The private Stash (kind 30001) is a single NIP-44 encrypted blob — nothing in * it is ever readable by others. To make a Stash item genuinely *public*, we * publish it as a SEPARATE, unencrypted NIP-99 classified listing (kind 30402), * exactly like the marketplace. This hook keeps those public events in sync with - * the encrypted Stash and lets any profile query another user's public quiver. + * the encrypted Stash and lets any profile query another user's public Stash. * * Hybrid model: * • Public + NOT for sale → showcase-only. Tagged `t: stash-showcase` with a - * `["showcase", "true"]` marker and `["price", "0", "SATS"]`. Shows on the - * profile Quiver, but is filtered OUT of the sellable marketplace. - * • Public + willingToSell/Trade → full listing. Shows on the profile Quiver - * (badged "For Sale") AND flows into the marketplace like any other listing. + * `["showcase", "true"]` marker and `["price", "0", "SATS"]`. Shows in the + * profile's public Stash, but is filtered OUT of the sellable marketplace. + * • Public + willingToSell/Trade → full listing. Shows in the profile's public + * Stash (badged "For Sale") AND flows into the marketplace like any listing. * * Each public event's `d` tag is the Stash item's stable `id`, so re-publishing * updates in place and we can delete (NIP-09) when an item goes private/removed. @@ -32,8 +32,8 @@ const LISTING_KIND = 30402; // NIP-99 Classified Listing const SHOWCASE_TAG = 'stash-showcase'; const D_PREFIX = 'stash-'; // public event d-tag = `stash-` -/** A public gear item as seen on someone's profile. */ -export interface QuiverItem { +/** A public gear item as seen on someone's profile (their public Stash). */ +export interface PublicStashItem { id: string; identifier: string; // d-tag pubkey: string; @@ -57,7 +57,7 @@ function tag(event: NostrEvent, name: string): string | undefined { } /** Parse a kind-30402 event that originated from a Stash item. */ -export function parseQuiverItem(event: NostrEvent): QuiverItem | null { +export function parsePublicStashItem(event: NostrEvent): PublicStashItem | null { if (event.kind !== LISTING_KIND) return null; // Must be one of our showcase events. const isShowcase = event.tags.some((t) => t[0] === 't' && t[1] === SHOWCASE_TAG); @@ -176,17 +176,17 @@ export function useStashShowcaseSync() { } /** - * Query a user's public quiver (public Stash items) for their profile. + * Query a user's public Stash items for their profile. * Returns showcase + for-sale items, newest first. */ -export function useUserQuiver(pubkey: string | undefined) { +export function useUserPublicStash(pubkey: string | undefined) { const { nostr } = useNostr(); return useQuery({ - queryKey: ['stash-quiver', pubkey], + queryKey: ['public-stash', pubkey], enabled: !!pubkey, staleTime: 60_000, - queryFn: async ({ signal }): Promise => { + queryFn: async ({ signal }): Promise => { if (!pubkey) return []; const timeout = AbortSignal.any([signal, AbortSignal.timeout(6000)]); @@ -204,11 +204,11 @@ export function useUserQuiver(pubkey: string | undefined) { if (!existing || ev.created_at > existing.created_at) byD.set(d, ev); } - const items: QuiverItem[] = []; + const items: PublicStashItem[] = []; for (const ev of byD.values()) { const status = tag(ev, 'status'); if (status === 'sold') continue; - const parsed = parseQuiverItem(ev); + const parsed = parsePublicStashItem(ev); if (parsed) items.push(parsed); } diff --git a/src/pages/NIP19Page.tsx b/src/pages/NIP19Page.tsx index d27aec8..d18156e 100644 --- a/src/pages/NIP19Page.tsx +++ b/src/pages/NIP19Page.tsx @@ -13,7 +13,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useUserSkateSpots } from '@/hooks/useSkateSpots'; import { useUserShops } from '@/hooks/useShops'; import { useUserCrews } from '@/hooks/useCrews'; -import { useUserQuiver } from '@/hooks/useStashShowcase'; +import { useUserPublicStash } from '@/hooks/useStashShowcase'; import { SpotCard, SpotCardSkeleton } from '@/components/SpotCard'; import { genUserName } from '@/lib/genUserName'; import NotFound from './NotFound'; @@ -45,7 +45,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) { const { data: spots, isLoading: spotsLoading } = useUserSkateSpots(pubkey); const { data: shops, isLoading: shopsLoading } = useUserShops(pubkey); const { memberships: crews, isLoading: crewsLoading } = useUserCrews(pubkey); - const { data: quiver, isLoading: quiverLoading } = useUserQuiver(pubkey); + const { data: publicStash, isLoading: publicStashLoading } = useUserPublicStash(pubkey); const [copied, setCopied] = useState(false); const metadata = author.data?.metadata; @@ -303,16 +303,16 @@ function ProfilePage({ pubkey }: { pubkey: string }) { )} - {/* The Quiver — public gear from this skater's Stash */} - {(quiverLoading || (quiver && quiver.length > 0)) && ( + {/* Public Stash — gear this skater chose to show publicly */} + {(publicStashLoading || (publicStash && publicStash.length > 0)) && (

- The Quiver - {quiver && quiver.length > 0 && ( + Stash + {publicStash && publicStash.length > 0 && ( - {quiver.length} + {publicStash.length} )}

@@ -323,7 +323,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) { )}
- {quiverLoading ? ( + {publicStashLoading ? (
{Array.from({ length: 3 }).map((_, i) => ( @@ -331,7 +331,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
) : (
- {quiver!.map((g) => ( + {publicStash!.map((g) => (
Crews

-

{quiver?.length || 0}

+

{publicStash?.length || 0}

Gear

diff --git a/src/pages/StashPage.tsx b/src/pages/StashPage.tsx index 78d96d8..c2c306b 100644 --- a/src/pages/StashPage.tsx +++ b/src/pages/StashPage.tsx @@ -421,7 +421,7 @@ function AddItemDialog({ onAdd, mode = 'stash', trigger }: { onAdd: (item: Omit<

{formData.visibility === 'private' && 'Encrypted — only visible to you.'} {formData.visibility === 'friends' && 'Kept private for now (friends-only sharing coming soon).'} - {formData.visibility === 'public' && 'Shown on your public profile in "The Quiver". If it\u2019s also for sale/trade, it appears in the marketplace too.'} + {formData.visibility === 'public' && 'Shown in your public Stash on your profile. If it\u2019s also for sale/trade, it appears in the marketplace too.'}

@@ -550,7 +550,7 @@ function EditItemDialog({ item, open, onOpenChange, onSave }: { item: StashItem;

{formData.visibility === 'private' && 'Encrypted — only visible to you.'} {formData.visibility === 'friends' && 'Kept private for now (friends-only sharing coming soon).'} - {formData.visibility === 'public' && 'Shown on your public profile in "The Quiver". If it\u2019s also for sale/trade, it appears in the marketplace too.'} + {formData.visibility === 'public' && 'Shown in your public Stash on your profile. If it\u2019s also for sale/trade, it appears in the marketplace too.'}