Rename "The Quiver" to "Stash" for brand consistency

Use the established "Stash" term everywhere instead of introducing a new
word. The profile section is now titled "Stash", visibility copy reads
"Shown in your public Stash on your profile", and internal identifiers
were renamed (useUserQuiver → useUserPublicStash, QuiverItem →
PublicStashItem, parseQuiverItem → parsePublicStashItem). NIP.md updated
to match.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-02 19:41:45 +07:00
parent 32a3cbca88
commit b3a49499bb
5 changed files with 32 additions and 32 deletions

8
NIP.md
View file

@ -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>", "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>", "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.

View file

@ -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);

View file

@ -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-<itemId>`
/** 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<QuiverItem[]> => {
queryFn: async ({ signal }): Promise<PublicStashItem[]> => {
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);
}

View file

@ -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 }) {
)}
</div>
{/* 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)) && (
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-black uppercase flex items-center gap-2">
<Package className="h-5 w-5 text-[#ff6600]" />
The Quiver
{quiver && quiver.length > 0 && (
Stash
{publicStash && publicStash.length > 0 && (
<Badge className="bg-[#ff6600]/20 text-[#ff6600] border-[#ff6600]/50">
{quiver.length}
{publicStash.length}
</Badge>
)}
</h2>
@ -323,7 +323,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
)}
</div>
{quiverLoading ? (
{publicStashLoading ? (
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-40 rounded-xl" />
@ -331,7 +331,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{quiver!.map((g) => (
{publicStash!.map((g) => (
<div
key={g.id}
className="group rounded-xl overflow-hidden border-2 border-gray-800 bg-black/60 hover:border-[#ff6600]/50 transition-all"
@ -496,7 +496,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
<p className="text-[10px] text-gray-500 uppercase">Crews</p>
</div>
<div className="text-center p-3 rounded-lg bg-black/40">
<p className="text-2xl font-black text-[#ff6600]">{quiver?.length || 0}</p>
<p className="text-2xl font-black text-[#ff6600]">{publicStash?.length || 0}</p>
<p className="text-[10px] text-gray-500 uppercase">Gear</p>
</div>
</div>

View file

@ -421,7 +421,7 @@ function AddItemDialog({ onAdd, mode = 'stash', trigger }: { onAdd: (item: Omit<
<p className="text-xs text-muted-foreground">
{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.'}
</p>
</div>
</div>
@ -550,7 +550,7 @@ function EditItemDialog({ item, open, onOpenChange, onSave }: { item: StashItem;
<p className="text-xs text-muted-foreground">
{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.'}
</p>
</div>
</div>