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:
parent
32a3cbca88
commit
b3a49499bb
5 changed files with 32 additions and 32 deletions
8
NIP.md
8
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>", "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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue