feat(bell): dedicated Trades tab

The bell had All/Social/News/Crews; trade steps were only inside All. Added a
5th tab (Handshake, orange accent) listing offers, counter-offers, outcomes and
trade confirmations, with its own count. All still shows them first.
This commit is contained in:
SkateSpots 2026-09-29 18:42:24 +07:00
parent 7dff8793b8
commit 0b7371263c
2 changed files with 38 additions and 3 deletions

View file

@ -89,7 +89,13 @@ session with **no other memory of this project**. So:
→ "Marketplace Offers". Trade/offer steps also feed **`useTradeNotifications`**
→ the bell's **All** tab (`TradeNotificationCard`, counted in the badge against
`seenAt`) and OS notifications (new **`trades`** category in `notificationPrefs`,
wired in `OsNotificationBridge`).
wired in `OsNotificationBridge`). The bell also has a dedicated **Trades tab**
(5th tab, `Handshake`) listing them, with its own count. **Barter is supported
by design**: trade attestations carry `method` (`cash|lightning|trade|other` —
`MarkSoldDialog` picks it), so a swap feeds reputation exactly like a sale; an
offer with **no `amount`** plus a note is how a swap is proposed (there is
deliberately **no** "For trade" listing type yet — `LISTING_TYPES` is
`selling|wtb`).
- **Cashu escrow** (marketplace): time-locked **P2PK** (lock to seller + buyer
refund key + locktime) — see **`ESCROW.md`** and `src/lib/cashuEscrow.ts`
(unit-tested). Protocol: `cashu_token` on the `31872`; each user advertises a

View file

@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
import { Bell, Users, Check, Loader2, EyeOff, Globe, CheckCheck, Heart, Radio, ArrowRight } from 'lucide-react';
import { Bell, Users, Check, Loader2, EyeOff, Globe, CheckCheck, Heart, Radio, ArrowRight, Handshake } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { ScrollArea } from '@/components/ui/scroll-area';
@ -347,7 +347,7 @@ export function NotificationBell({ className }: { className?: string }) {
</div>
<Tabs defaultValue="all" className="flex min-h-0 w-full flex-1 flex-col">
<TabsList className="grid w-full shrink-0 grid-cols-4 rounded-none border-b border-white/10 bg-transparent p-0">
<TabsList className="grid w-full shrink-0 grid-cols-5 rounded-none border-b border-white/10 bg-transparent p-0">
<TabsTrigger
value="all"
className="rounded-none border-b-2 border-transparent py-2.5 text-xs font-bold data-[state=active]:border-teal-400 data-[state=active]:bg-transparent data-[state=active]:text-teal-400"
@ -355,6 +355,13 @@ export function NotificationBell({ className }: { className?: string }) {
All
{badgeTotal > 0 && <span className="ml-1 text-teal-400">{badgeTotal}</span>}
</TabsTrigger>
<TabsTrigger
value="trades"
className="rounded-none border-b-2 border-transparent py-2.5 text-xs font-bold data-[state=active]:border-[#ff6600] data-[state=active]:bg-transparent data-[state=active]:text-[#ff6600]"
>
<Handshake className="mr-1 h-3 w-3" /> Trades
{tradeNotifs.length > 0 && <span className="ml-1 text-[#ff6600]">{tradeNotifs.length}</span>}
</TabsTrigger>
<TabsTrigger
value="social"
className="rounded-none border-b-2 border-transparent py-2.5 text-xs font-bold data-[state=active]:border-teal-400 data-[state=active]:bg-transparent data-[state=active]:text-teal-400"
@ -430,6 +437,28 @@ export function NotificationBell({ className }: { className?: string }) {
</div>
</TabsContent>
{/* ─── TRADES (offers, counter-offers, confirmations) ──────── */}
<TabsContent value="trades" className="m-0 p-3">
{tradeLoading && tradeNotifs.length === 0 && (
<div className="flex items-center justify-center gap-2 py-8 text-sm text-gray-500">
<Loader2 className="h-4 w-4 animate-spin" /> Checking offers and trades…
</div>
)}
{!tradeLoading && tradeNotifs.length === 0 && (
<EmptyState label="Offers, counter-offers and trade confirmations all land here — whether you're selling, buying or swapping." />
)}
<div className="space-y-2.5">
{tradeNotifs.map((n) => (
<TradeNotificationCard
key={n.id}
notification={n}
unread={n.createdAt > seenAt}
onNavigate={() => setOpen(false)}
/>
))}
</div>
</TabsContent>
{/* ─── SOCIAL ──────────────────────────────────────────────── */}
<TabsContent value="social" className="m-0 p-3">
{activityLoading && activity.length === 0 && (