Commit graph

471 commits

Author SHA1 Message Date
shakespeare.diy
1cb7c109e7 The Lab: warn before a "Become a …" form silently overwrites an existing profile (1.2.28)
Lab provider profiles (coach, filmer, editor, photographer, builder) are
addressable events with a fixed per-type `d` identifier, so re-publishing
REPLACES the existing profile. Unlike BecomeSkaterPage — which pre-fills from
the existing profile so re-submitting edits rather than clobbers — these five
forms start blank and reconstruct nothing. A returning provider who opened the
form again and submitted would silently wipe every field they'd previously set
(bio, rate, spots, contact, links) down to whatever they happened to retype.

Rather than fragile per-page reconstruction of the mixed services/skills tag
sets (high regression risk), added a small honest guardrail:

- New ExistingLabProfileNotice component: appears only when the current user
  already has a profile of that type (looked up via useLabProfile by their
  pubkey + the known identifier). It states plainly that publishing will
  REPLACE the current profile and links to view the existing one first.
- Wired into all five Become* pages with the correct identifier/label.

Also confirmed (not a bug): all five navigate to /coach/:pubkey/:identifier
after publish — CoachDetailPage is a generic Lab-profile renderer that resolves
any type by the distinct `d` identifier, so this is correct despite the name.

Investigation of the shared submit handlers found them otherwise consistent
and well-formed; left untouched.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 22:06:49 +07:00
shakespeare.diy
2b3ee73292 Wallet QR + mint UX: honest scan feedback, truthful auto-distribute affordance (1.2.27)
QR scanner (CashuWalletCard.handleQRScanResult):
- The classifier recognizes bitcoin_address, npub, and CLINK offer/debit codes,
  but all of them fell through to a misleading "Could not recognize the scanned
  content" toast. Now each gets an honest, specific message:
    • Bitcoin address → "On-chain not supported here" (this is an ecash/Lightning
      wallet with no on-chain send)
    • npub → "That's a Nostr profile"
    • noffer/ndebit → "Payment code not supported yet"
  Only genuinely unclassified input shows the generic "unrecognized" message.

MintPrivacyManager:
- Auto-distribute toggle was a live Switch that flipped on and then admitted
  "Coming soon — does nothing." Replaced with a disabled switch + "Soon" badge
  so users can't think they enabled a feature that isn't real. Removed the now
  dead autoDistribute state.
- Dropped unused imports (Progress, Lock, Eye, EyeOff).

The mint manager itself (distribution viz, privacy score, trust/balance
removal warnings) was already excellent and left untouched.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 21:54:43 +07:00
shakespeare.diy
2b50f2b496 Spot form: enforce required spot type, guard submit during Quick Snap upload, drop debug log (1.2.26)
CreateSpotPage:
- Spot type is marked required (*) and drives the quality meter, but submit
  never validated it — a spot could be published with no type. Now validated
  with a clear toast, matching the title/coordinate checks.
- Submit was allowed while the Quick Snap photo was still uploading in the
  background, which would publish the spot WITHOUT the photo the user just
  took. Submit is now blocked (with an "Uploading photo…" button state and a
  reassuring toast) until the upload settles.
- Removed a leftover console.log of the full event payload.

EditSpotPage:
- Same missing spot-type validation (also marked * in its UI) — added the
  same guard for consistency.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 21:51:59 +07:00
shakespeare.diy
8affd02bcf Crew dismissals: merge remote set before sync (close clobber pattern)
Third and final surface with the encrypted-replaceable-list clobber pattern
(after Stash and Contacts). useDismissCrewNotification's cross-device sync path
built the published set from cache-or-local-mirror, which are both empty on a
fresh device before the useCrewDismissals read query resolves. A non-interactive
user dismissing a notification in that window would publish a set with only the
new id, overwriting — and wiping — the previously-synced dismissals from other
devices.

Fix: before encrypting/publishing, re-fetch + decrypt the remote dismissal set
and union it with the pending id (and keep the local mirror + cache aligned).
On read failure it still publishes the local union — dismissals are additive, so
we can only ever under-merge, never silently wipe.

Lower stakes than Stash/Contacts (worst case: a handled invite re-appears in the
bell on another device, not real data loss), but it keeps the fetch-before-write
discipline consistent across every encrypted list in the app. Amber path is
unchanged — still local-only, zero extra prompts.

Bumps to 1.2.25 (Android versionCode 10225 auto-derived).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 20:46:46 +07:00
shakespeare.diy
a31583079b Private Contacts: fetch-before-write to prevent list clobber
Same encrypted-replaceable-list data-safety pattern as the Stash fix, applied
to usePrivateContacts (kind 30000, NIP-44 encrypted to self).

add/remove built the new list from the React Query cache, which is empty until
the Contacts page has decrypted — and with Amber the read query is gated /
staleTime:Infinity. Saving a contact before that decrypt (fresh device, or if a
"save privately" action is ever wired up elsewhere) could publish a list with
only the new pubkey and silently wipe every other private contact.

Fix: add/remove now call fetchLatest() — a fresh relay fetch + decrypt of the
current list immediately before publishing — and union it with the warm cache
so an optimistic just-added pubkey is never dropped. On fetch/decrypt failure
it falls back to the cache rather than risk a wipe.

UX: clarified the contacts search placeholder ("Paste an npub to filter…") so
it no longer implies name search that the npub-only filter doesn't do. Left the
star toggle un-confirmed on purpose — it's a low-stakes, one-tap-reversible
action and a dialog would only add friction. ContactsPage is otherwise already
clean/elegant (tabbed, skeletons, thoughtful empty states, encryption notice).

Bumps to 1.2.24 (Android versionCode 10224 auto-derived).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 20:36:01 +07:00
shakespeare.diy
29d41b81eb Stash: prevent data-loss clobber on save + confirm before deleting gear
Audited the Stash — a private, NIP-44-encrypted, Amber-sensitive surface.

DATA-SAFETY FIX (the important one):
The stash is a single replaceable event whose encrypted content is the WHOLE
item list, so every add/update/remove rewrites the entire event. The mutations
built the new list from the `stash` closure value — but that's legitimately
`undefined` before the decrypt completes, and with Amber the read query is
gated / staleTime:Infinity, so it may never have been decrypted at mutation
time. Result: adding one item while `stash` was empty/undefined could publish a
list containing only that item and silently WIPE all existing gear.

Fix: mutations now call `fetchLatestStash()` — a fresh relay fetch + decrypt of
the current stash immediately before saving — and build the new list on top of
that. It falls back to React Query cache (never an empty list) on failure, and
throws rather than risk a clobber if it truly can't load. saveStash also writes
the just-published value straight into the cache to keep reads consistent.

The extra decrypt per mutation is an acceptable Amber tradeoff: a mutation is
already a deliberate write (encrypt + sign prompt), and data integrity beats
prompt count.

UX:
• Delete now asks for confirmation via an AlertDialog (item name + a note that
  public items get unlisted). Previously a single tap wiped an item outright —
  destructive and, with Amber, a surprise signer prompt.

The rest of StashPage (login gate, encryption-required state, distinct
wishlist/stash/filtered empty states, skeletons, rarity cards, grid/list) is
already excellent — left as-is.

Bumps to 1.2.23 (Android versionCode 10223 auto-derived).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 20:31:48 +07:00
shakespeare.diy
7f49df1503 Wallet UX polish: elegant recovery-phrase reveal, separate amounts, loading states
Continued the wallet audit with a focus on intuitive, minimal, elegant UX.

• Recovery phrase — replaced the raw browser alert() (which dumped the seed
  as unstyled, uncopyable plaintext) with a proper RecoveryPhraseDialog:
  hidden/blurred by default with a deliberate "tap to reveal" gate (no
  shoulder-surfing on open), a numbered 12-word chip grid for accurate
  transcription, a copy button (enabled only after reveal), and an honest
  "paper is safest, clipboards leak" note. Re-hides on every close. This was
  the wallet's most sensitive surface and its worst blemish.

• Fixed shared-state bug: "Create Cashu Token" and "Pay Lightning Address"
  both wrote to the same `sendAmount`, so switching between them carried a
  stale amount across two unrelated intents. The address form now owns its
  own amount state.

• Added loading feedback + double-tap protection to every money action that
  lacked it — Generate Invoice ("Generating…"), Create Token ("Creating…"),
  Receive Token ("Receiving…"), Pay Lightning Address ("Sending…"). Each hook
  now exposes an in-flight flag and guards re-entry.

WalletPage and the browser "app-only" notice were already clean/elegant —
left as-is.

Bumps to 1.2.22 (Android versionCode 10222 auto-derived).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 20:20:51 +07:00
shakespeare.diy
39ac517610 Fix Cashu zap: pay in one click + honor custom-amount balance guard
Audit of the wallet/zap flow surfaced a real money-path bug in ZapDialog's
Cashu branch:

• One-click Cashu zap never worked. The Cashu payment branch only ran
  `if (... && invoice)`, but no invoice exists on the first click — so
  selecting "Cashu" and tapping Zap silently fell through to the Lightning
  flow (generating an invoice / paying via NWC-WebLN) and the user's Cashu
  balance was never used. Now the Cashu path generates the recipient's
  amount-verified zap invoice and melts it from the Cashu wallet in a single
  click.

  To support this cleanly, useZaps now exposes `generateZapInvoice()` —
  builds + signs the NIP-57 request, fetches the LNURL invoice, and runs the
  same `assertInvoiceAmount` security check the Lightning rail uses — WITHOUT
  auto-paying. The Lightning `zap()` path was refactored to use it too, so
  both rails share one verified-invoice codepath.

• Custom-amount balance guard bypassed. The insufficient-balance warning and
  the disabled Send state only checked `typeof amount === 'number'`, but the
  custom-amount input stores a string — so a typed amount over the Cashu
  balance slipped past both. Both now coerce to a number first.

• Smarter defaults: when the dialog opens and the user has Cashu funds but no
  connected Lightning wallet, Cashu is pre-selected and the "connect a wallet"
  setup nag is skipped (they can already pay).

• Removed a dead `useLightningOperations` import.

Also audited mobile layout (Layout.tsx — mature, safe-area-aware, no changes
needed) and ZapCrewDialog (uses the separate useCrewZap splitter, no Cashu
option, no bug).

Bumps to 1.2.21 (Android versionCode 10221 auto-derived).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 20:15:10 +07:00
shakespeare.diy
48b127f7b0 Amber UX: honest multi-approval hint for encrypted chat/DM sends
Audit outcome for the two suspected "double-prompt" Amber flows:

• Image upload — CONFIRMED already a single signature. BlossomUploader
  (even at the pinned 0.47.1) signs one kind-24242 auth event and reuses
  it across all servers via Promise.any, so uploads = 1 Amber prompt. No
  change needed.

• DM / crew-chat send — the multiple prompts are protocol-inherent and
  MUST NOT be optimized away: NIP-17 seals+signs once per recipient (and
  to self); a Cordn crew message signs an author proof then encrypts once
  per roster member. Reducing the count would weaken confidentiality or
  authorship. So instead of a code shortcut we add transparency.

New <AmberApprovalHint>: a tiny heads-up shown ONLY to interactive
(Amber) signers before an inherently multi-approval action, telling them
how many Approve screens to expect and that it's normal. Renders nothing
for silent signers (extension/nsec/bunker), where the ops are invisible.

Wired into:
  • CrewChat composer — count derived from transport + roster size
    (Cordn: roster+1; NIP-17: roster*2).
  • ConversationPage (1:1 DM) composer — 4 approvals (encrypt+sign to the
    recipient and to yourself).

Dependencies: reviewed the full manifest; deliberately left unchanged.
Every upgrade candidate is either on the critical crypto/Nostr/payments
path (nostrify, nostr-tools, cashu, bitcoinjs, noble, scure — pinned for
safety) or a documented major bump (lucide v1, react 19, react-router 7)
with breaking changes and no user-facing benefit. No security advisories
force a change, and caret ranges already pull safe patch/minor updates.
"It must work" wins over churn.

Bumps to 1.2.20 (Android versionCode 10220 auto-derived).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 20:08:39 +07:00
shakespeare.diy
56f29e0a29 Amber: make crew-notification dismissal local-only for interactive signers
Accepting/declining a crew invite (and handling join-requests) fired an
encrypted NIP-78 cross-device sync on every dismissal, costing an extra
encrypt + sign Amber approval on top of the required membership signature.

For interactive (Amber) signers, dismissal is now kept local-only:
  • Accept invite  → 1 Amber prompt (membership attestation only)
  • Decline invite → 0 Amber prompts (local dismissal only)

The dismissal still applies instantly and permanently on-device; only the
cross-device "clear it on my other devices too" nicety is dropped for Amber.
Extension/nsec/bunker signers keep the silent encrypted sync (free for them).

Bumps to 1.2.19 (Android versionCode 10219 auto-derived).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 20:02:45 +07:00
shakespeare.diy
c9b0571de9 feat(ux): graceful image fallbacks for broken content images (1.2.18)
Added <ImageWithFallback> — a drop-in <img> that shows a subtle placeholder
(skate emoji or a caller-supplied icon) instead of the browser's broken-image
glyph when a user-supplied URL is missing or 404s. Fills the same layout box so
nothing shifts.

Swapped it into the content-image surfaces that display user-uploaded URLs:
- Marketplace item cards (grid + purchase dialog)
- Spot detail (hero + gallery thumbs) and map list/popup cards
- Shop detail (hero + gallery)
- Crew & Lab cross-link cards (icon fallback)
- Crew agent panel, newsletter issue view + notification bell newsletter thumb
- Stash gear cards (list + grid), profile banner (NIP19), newsletter spot cards

Left untouched: avatars (shadcn Avatar already has fallbacks), QR codes and
rider-card/print images (data URLs, never 404), and in-form upload previews
(local object URLs).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 19:46:40 +07:00
shakespeare.diy
4aec9539d6 fix(ux): repair broken in-app navigations under HashRouter
Three places used window.location.href to navigate to bare paths (/signers,
/messages/:npub, /settings). Under HashRouter those trigger a full page reload
to a non-hash path the router can't match — the user got a blank/404 flash
instead of the target screen.

- WhosSkating: DM button now uses useNavigate() instead of a reload.
- StashPage: "Learn About Signers" now uses <Link to="/signers"> (asChild).
- BackupReminder: "Back up now" now uses navigate() AND corrects the deep-link
  param from ?tab=security to ?section=security (SettingsPage reads ?section),
  which previously dropped the user on the settings root instead of Security.
  Also closes the reminder sheet on navigate.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 19:22:40 +07:00
shakespeare.diy
28cd59916a fix(amber): stop global SasquatchGuide from decrypting the stash on every page
usePersonalContext (consumed by SasquatchGuide, mounted in the app-wide Layout)
eagerly called useStash(), firing the encrypted-stash NIP-44 decrypt on EVERY
page load — one Amber popup per navigation. useStash now accepts { enabled },
and usePersonalContext passes enabled:false so it only reads the stash from
cache (populated when the user actually opens the Stash page). The AI's
getMyStash tool degrades gracefully to cached data. No behaviour change for
silent signers.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 19:15:00 +07:00
shakespeare.diy
b910141699 fix(amber): stop passive decryption from storming Amber on navigation (1.2.16)
With Amber as signer, every nip44 decrypt is a foreground intent (visible
popup). Several hooks decrypt inside auto-running queries, so Amber flashed on
navigation/focus and (worst case) storms — one popup per encrypted event, and
polling queries re-fired every few seconds.

New helper isInteractiveSigner() (signerCapabilities.ts) detects the Amber
(x-amber) signer, where each crypto op prompts the user. Hooks now gate passive
decryption for interactive signers only; extension/nsec/bunker keep their
silent, live-polling behaviour.

Fixes:
- useCrewDismissals (GLOBAL via header NotificationBell — the main offender):
  Amber users now use the instant localStorage mirror only, never passively
  decrypt the synced list; disabled refetchOnWindowFocus + Infinity staleTime.
  Dismissals still sync OUT on the write path.
- useDirectMessages (all 4 DM query hooks): added amberSafeQueryOptions() —
  no background polling and no focus refetch for Amber (these decrypt up to
  ~200 gift wraps × 2 each); silent signers keep the 60s poll.
- useStash / usePrivateContacts: disabled focus refetch + Infinity staleTime
  for Amber so the single decrypt runs once, not on every focus.
- useCordnChat: dropped the 5s live poll for Amber (was a popup every 5s);
  silent signers keep it.

Scanned every signEvent call site: all are inside mutations/user callbacks
(zaps, invites, DMs, chat, follows, purchases, NIP-98 auth) — no passive
auto-signing anywhere. Deliberate user actions still prompt Amber as intended.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 14:57:35 +07:00
shakespeare.diy
63db160b80 fix(amber): Amethyst-style foreground-intent signer to end the GrapheneOS sign hang
Root cause: the stock nostr-signer-capacitor-plugin (0.0.5) tries Amber's
background ContentProvider first, which GrapheneOS blocks and never returns,
with no clean fallback — so signing hung 90s and Amber never appeared.

Fix (matches Amethyst, the reference NIP-55 client):
- New native Capacitor plugin AmberIntent (android-templates/AmberIntentPlugin.java)
  that goes STRAIGHT to a foreground nostrsigner: intent via a properly-registered
  ActivityResultLauncher, tracks each request by id, and times out gracefully.
- MainActivity now registers the launcher in onCreate (AndroidX contract) and
  hands it to the plugin, while staying a safe stock BridgeActivity (no
  WebViewClient override → no black screen).
- inject-main-activity.mjs now installs both MainActivity.java and AmberIntentPlugin.java.
- capacitorAmberSigner.ts rewired to registerPlugin('AmberIntent'); every op is a
  user-visible Amber Approve/Reject popup; rejections surface as a friendly message.
- Removed the dead npm nostr-signer-capacitor-plugin dependency (and its phantom
  coracle fork spec) plus its vite external entry — signing no longer uses it.
- Delete flow copy updated to the new foreground UX (Approve in Amber / Cancelled
  on reject) instead of the diagnostic SIGN/PUBLISH narration.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 12:39:00 +07:00
shakespeare.diy
be05a603be security: verify invoice amounts + require provider-issued zap receipts (1.2.14)
Fixes the audit findings:

HIGH-1 (NWC/WebLN auto-pay of unverified invoices): add assertInvoiceAmount and
call it before every programmatic payment in useZaps, useCrewZap, and
SpamFeePayment. A malicious LNURL/Lightning-address server can no longer return
an inflated invoice and drain the wallet — amount is decoded from bolt11 and must
match the intended sats (±1 sat), amountless invoices are rejected.

HIGH-2 (forgeable trust signals):
- useSasquatchPro: Pro is granted ONLY by a zap receipt AUTHORED by the
  platform's Lightning provider, embedding a user-signed zap request that tags
  the platform, with a real paid amount >= smallest tier. The self-signed
  kind:32671 receipt is now advisory only and never grants access (closed the
  "self-grant permanent Pro" hole).
- useFeaturedVerification: a listing is 'verified' only when the matching 9735 is
  authored by the provider pubkey (not any self-published receipt).
- useCrewGoal: tally only invoice-backed (bolt11) zaps; the forgeable amount tag
  is no longer summed, so goal progress can't be inflated with fake receipts.

Bump to 1.2.14.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-07 11:45:21 +07:00
shakespeare.diy
850c37dbaa diag(delete): narrate SIGN vs PUBLISH phases on-screen to locate the hang (1.2.13)
We can't identify where delete freezes. Split the flow into two independently
timed, on-screen-narrated phases: Step 1 SIGN (Amber) and Step 2 PUBLISH
(relays). Whichever toast is on screen when it hangs tells us exactly which layer
is at fault — Amber vs relay — so we fix the right thing. Bump to 1.2.13.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-04 23:07:13 +07:00
shakespeare.diy
52fd6f7a81 fix(delete): single signature for admin takedown — stop Amber collision hang (1.2.12)
Admin delete fired THREE signed events (kind:5 deletion, kind:1985 label, NIP-86
banevent). Each opens Amber, but Amber handles one approval at a time — the
extra background signs collided with the primary one on Android and hung the
whole delete ("taking longer than expected"). Now the admin path publishes only
the kind:5 deletion (one Amber approval) and hides locally (no signature); the
optional label/relay-ban are dropped from the interactive flow. Also raise the
awaited timeout 40s→120s to fit a real Amber round-trip. Bump to 1.2.12.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-04 22:56:38 +07:00
shakespeare.diy
50914b7b9b fix(amber): call setPackageName before signing so the plugin opens Amber (1.2.9)
Verified against the plugin's Java source: signEvent/nip*Encrypt/Decrypt
IMMEDIATELY reject with "Signer package name not set" when no package is
configured — which is exactly what my earlier "omit packageName" change caused,
and that rejection surfaced as the endless spinner on delete/accept/zap.

With the package set, the plugin tries the content resolver, and when that
returns null (permission not remembered) it falls back to
startActivityForResult(nostrsigner:) — the SAME native call login uses that
reliably opens Amber. Restore native plugin signEvent/nip04/nip44 with
setPackageName + AMBER_PACKAGE, reconstruct signature-only responses, and remove
the unused JS-intent bridge. Bump to 1.2.9.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-04 21:44:27 +07:00
shakespeare.diy
fea9ab3091 fix(android): native intent hand-off so Amber actually opens for signing (1.2.8)
Amber never appeared for delete/accept-invite/zap because Capacitor's default
WebView silently drops JS-initiated intent:/nostrsigner: navigations — the
request that opens Amber never reached the OS. Login worked only because it
uses the plugin's native startActivityForResult.

Add a custom MainActivity (android-templates/MainActivity.java) that overrides
shouldOverrideUrlLoading to launch intent:/nostrsigner: URLs via startActivity()
— a real native launch that opens Amber on GrapheneOS, matching login. Injected
into the generated android/ project at build time by inject-main-activity.mjs,
wired into android:build/sync/init. Simplify the JS launcher to navigate the
WebView (now intercepted natively). Bump to 1.2.8.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-04 21:23:38 +07:00
shakespeare.diy
0b50fbb91a fix(amber): drive native signing/encryption via OS Intent + deep-link callback
The capacitor plugin's signEvent/encrypt query Amber's ContentProvider first,
which stalls on GrapheneOS and never launches Amber's approval UI — causing
delete, accept-invite, zaps, and every signing action to hang forever.

Add amberIntentBridge.ts which drives Amber through the reliable
intent:...scheme=nostrsigner flow (the same path login uses that actually opens
Amber), returning a promise resolved in-process via the org.skatespots.app://
deep-link callback (DeepLinkHandler → resolveAmberIntent). CapacitorAmberSigner
now routes signEvent/nip04/nip44 through this bridge instead of the stalling
ContentResolver path. Fast 60s fail-safe with a clear message instead of an
indefinite spinner.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-04 21:03:52 +07:00
shakespeare.diy
b6a37f8bbb Request all Amber permissions at login (fixes silent no-prompt signing)
The likely real cause of "Amber never pops up, delete hangs" on
GrapheneOS: login only requested the get_public_key permission, so later
sign_event / encrypt / decrypt calls hit a per-action permission check
that routes through Amber's ContentProvider and stalls WITHOUT showing a
prompt. Now login pre-grants get_public_key, sign_event, nip04/nip44
encrypt+decrypt up front, so subsequent signing is a fast background
round-trip with no surprise permission stall.

IMPORTANT: existing sessions only granted get_public_key — users must log
out and back in once for Amber to grant the new permission set.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 23:49:03 +07:00
shakespeare.diy
96e2939451 Fix notification badge sticking after "Mark read"
The badge counted ALL actionable crew items (invites, join requests,
pending confirmations) regardless of read state, so "Mark read" (and even
opening the bell) never cleared it. Now the badge counts only items newer
than a per-account "seen at" timestamp that "Mark read"/open updates.
Actionable crew cards still render in their tab until acted upon — only
the red badge clears — which is the expected behavior.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 23:46:39 +07:00
shakespeare.diy
244cfcf787 Fix Amber never prompting on sign/encrypt (GrapheneOS ContentProvider stall)
ROOT CAUSE of "stuck on Deleting…" with no Amber prompt:
signEvent (and nip04/nip44 encrypt/decrypt) passed packageName to the
native plugin. With a packageName set, the plugin first tries Amber's
ContentProvider — a background, no-UI query that stalls on GrapheneOS and
never launches Amber's approval screen. So the user saw NO prompt and the
request timed out ("takedown taking longer than expected").

Login already worked around this by omitting packageName so the plugin
goes straight to startActivityForResult(nostrsigner:) which shows Amber's
UI. This mirrors that fix for every signing/crypto op: use the raw native
signer with NO packageName. Removed the now-unused getPlugin /
ensurePackageName / setPackageName machinery.

This is why deletes (and any signed action / encryption) never surfaced
Amber and hung.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 23:33:17 +07:00
shakespeare.diy
c3fc033b37 Add real relay-level takedown (NIP-86 banevent) for admin deletes
The community relay goskatespots.nostr1.com runs on relay.tools, which
implements the NIP-86 Relay Management API. As the relay owner/moderator,
an admin can genuinely remove an event from the relay's database via
`banevent` — unlike a NIP-09 kind:5 request that most relays ignore for
non-authors.

- New src/lib/relayAdmin.ts: NIP-86 JSON-RPC client with NIP-98 HTTP auth
  (kind:27235 signed by the user's signer, payload hash included per
  NIP-86). Includes banEventOnRelay + getRelaySupportedMethods, with a
  CORS-proxy fallback for WebView contexts.
- useDeleteEntity admin path now also fires banEventOnRelay in the
  background (best-effort, never blocks the spinner), so admin takedowns
  actually erase the event from our own relay in addition to the kind:5
  request, optimistic local hide, and cross-device kind:1985 label.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 23:07:14 +07:00
shakespeare.diy
79e8376f58 Make admin delete non-blocking + polish notification bell
DELETE HANG (real fix):
The admin takedown required TWO sequential Amber approvals (kind:5
deletion + kind:1985 hide label) and awaited both. On mobile the second
Amber prompt often doesn't resurface, so the button sat on "Deleting…"
until the 90s signer timeout. Now:
- Only the kind:5 deletion is awaited (the required action).
- The target is recorded in a local optimistic-hide store immediately, so
  the app hides it on this device the instant kind:5 succeeds — no waiting
  on a second signature.
- The cross-device kind:1985 label is fired in the BACKGROUND (not
  awaited); a second approval, if needed, happens after the dialog closes.
- The whole mutation is wrapped in a 40s hard timeout so the spinner can
  never hang indefinitely, whatever the signer/relay does.
- useAdminModeration merges the local optimistic set so hides are instant
  and survive reload.

NOTIFICATION BELL UI:
- Brand-lime (#ccff00) glowing badge with ring, replacing the flat teal dot.
- Bell wiggles + soft glow pulse when there are unread items (new `wiggle`
  keyframe).
- Gradient header with an icon chip, inline count, and a pill "Mark read"
  button / "All clear" state.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 22:59:16 +07:00
shakespeare.diy
36845b122d Add dependency-free BOLT11 amount decoder; drop removed nip57 helper
Newer nostr-tools removed nip57.getSatoshisAmountFromBolt11, breaking
useZaps and useCrewGoal. Add src/lib/bolt11.ts (getSatoshisFromBolt11)
that parses the sat amount straight from the invoice's human-readable
prefix — no external dependency, immune to nostr-tools API churn — and
use it in both hooks.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 22:20:27 +07:00
shakespeare.diy
144227b88b Fix zap request construction for newer nostr-tools types
The clean node_modules reinstall pulled newer @nostrify/* and nostr-tools
minors whose nip57.makeZapRequest signature changed (event param became a
required full NostrEvent), breaking tsc in useZaps and usePurchasePro.

Build the kind:9734 zap request events directly per NIP-57 instead of
relying on nip57.makeZapRequest, so we're decoupled from that helper's
changing type signature. Preserves correct target tagging:
- addressable events → 'a' tag (kind:pubkey:dTag)
- other events       → 'e' tag (event id)
- profile-only (pro purchase) → no event tag, keeps pro-tier/pro-expiry.

Pin @nostrify/nostrify, @nostrify/react and nostr-tools so the tree
stays reproducible.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 22:14:46 +07:00
shakespeare.diy
f730f0b200 Harden all Amber signing paths against empty-id / bare-signature responses
Extends the admin-delete hang fix across the whole app. Audited every
signing route:

- Native (CapacitorAmberSigner): already fixed — computes id locally.
- Web/PWA (AmberSigner redirect flow): the callback handlers did a raw
  JSON.parse(result), but Amber returns a BARE signature (returnType=
  'signature'), so this threw or produced an event with an empty id that
  stalls the relay — the same "stuck" defect as native. Added a shared
  reconstructSignedEvent() helper that rebuilds the event from the stored
  template + signature and computes the id via getEventHash. Applied in
  both AmberCallbackPage and AmberCallbackHandler.
- NIP-17 DMs, zap requests, vouches, crew invites, membership, etc. all
  sign via user.signer.signEvent and therefore inherit the fix.
- nsec / bunker / extension signers use Nostrify base classes (compute
  ids internally) — unaffected.
- Native nip04/nip44 encrypt/decrypt are timeout-protected — no hang.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 22:05:25 +07:00
shakespeare.diy
977d71dc60 Fix admin delete hanging on "Deleting…" with Amber signer
Compute event id locally in CapacitorAmberSigner.signEvent instead of
relying on Amber to echo it back. Amber (NIP-55) commonly returns only a
bare `signature` for kind:5 deletions and kind:1985 moderation labels,
leaving id empty — which failed the malformed-event check or emitted a
bad event that stalled the relay forever (the "stuck on Deleting…"
symptom). The id is a deterministic NIP-01 hash of the signed fields, so
we derive it ourselves, making signature-only responses work for every
kind including admin takedowns.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 21:56:26 +07:00
shakespeare.diy
a0673a3244 Fix ESLint gate: scope ignores to app source (exclude android/.agents/cvm-server/public html/scripts), add index.html title, downgrade unused-vars & no-explicit-any to warnings, fix real lint errors (SkateGlobe unused-expr, EventsPage prefer-const, LicensePage Infinity shadow, useless-escape in SasquatchGuide/RiderCardPage, stale eslint-disable directives)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 21:30:56 +07:00
shakespeare.diy
b2339a8fab Fix final 2 type errors: cast profile through unknown in CoachDetailPage, remove invalid variant/size props from ZapButton in TourDetailPage
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 21:23:32 +07:00
shakespeare.diy
be95def874 Fix runtime crash: leftover allComments references in MapPage/EventsPage useMemo deps after useCommentCounts rename
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 21:20:12 +07:00
shakespeare.diy
aea66d11e2 Fix 78 pre-existing TypeScript errors surfaced by npm test
- useDirectMessages: correct nip44 getConversationKey (Uint8Array) + finalizeEvent EventTemplate (remove pubkey/unused ephemeralPubkey/bytesToHex)
- Access current-user metadata via useCurrentUser() spread instead of user.metadata across ~10 files (rename to userMetadata where author metadata already bound)
- useCrewGoal: explicit UseQueryResult<CrewGoal|null> return type (fixes CrewForm/CrewTreasuryCard {} inference)
- New useCommentCounts hook; fix EventsPage/MapPage misuse of useComments(string[])
- CoachDetailPage: typed pget/pbool accessors for craft-specific union fields
- MapPage: shop entries include tags/category to satisfy SkateSpot
- SkateGlobe: read event.tags; LabPage: remove duplicate serviceType; LabCrewsStrip: add 'judging'
- Icon prop widened to accept style; ReportDialog subtype guard; SecuritySettings/TourCard/EventsPage comparisons; CashuWallet onClick; ConversationPage recipients; useLoggedInAccounts NLoginType import; LicensePage icon; TourDetailPage ZapButton target + real npub link; TestApp useSkateRelay

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 21:18:02 +07:00
shakespeare.diy
5e8544316b Audit fixes: harden Amber signEvent response handling, fix web AmberSigner signEvent naming, add Blossom upload fallbacks, sanitize untrusted URLs in FoodPage/CreateTourPage
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 20:20:08 +07:00
shakespeare.diy
5017c1b7ff Fix mobile bugs: DM/crew-chat crash, notification popover clipping, multi-account switch
#2 (crash): useDirectMessages called getEventHash() without importing it —
`getEventHash is not defined` broke crew chat and any NIP-17 DM send. Added
the missing import from nostr-tools.

#3 (bell not loading on phone): the notifications popover had a fixed-height
scroll area and no collision handling, so on small screens it rendered clipped
against the top edge with the tab content cut off. The popover now constrains
to the Radix available-height, uses a flex column, and the tab list/scroll area
flex correctly — all tabs (incl. Crews/News) are reachable on mobile.

#4 (multi-account on phone): the mobile header user menu had only Set Status +
Log Out. Added an Accounts section that lists other logged-in npubs to switch
between, plus "Add another account" which opens the login dialog. Desktop
already had this via AccountSwitcher; mobile now reaches parity.

Note: #1/#5/#6 (stuck on publish/vouch/delete) were reported against the shipped
1.2.2 APK, which also contained the #2 crash. Rebuild 1.2.3 and re-test — the
sign/publish paths in current source are correct (Amber round-trip has a 90s
timeout + surfaced errors).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 20:09:01 +07:00
shakespeare.diy
dc410106b3 Newsletter: subscriber count + admin archive backfill to Nostr
Two follow-ups to the Nostr-native newsletter (skipped auto-DM by design —
too spammy/fragile for now; the bell mailbox already covers the inbox feel).

Subscriber count (social proof):
- useNewsletterSubscriberCount tallies public NIP-78 subscription markers
  (deduped per author, counting current "true" state).
- Shown under the subscribe CTA as "Join N+ skaters getting the drops",
  hidden until it's meaningful (>=3) and rounded once it grows.

Archive backfill (consistency without losing the rich page):
- lib/newsletterArticle.ts converts a static NEWSLETTER_ISSUES entry into a
  NIP-23 (kind 30023) article — structured sections rendered to Markdown,
  stable slug `issue-N`, published_at from the issue date.
- NewsletterBackfillPanel (admin-only) diffs the static archive against what's
  already on-relay and publishes only the missing issues; safe to re-run,
  reports partial failures. The rich static page rendering is untouched.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 19:21:09 +07:00
shakespeare.diy
08f74d7991 Add Nostr-native newsletter: subscribe, mailbox & notifications
Turn the static newsletter into a live, Nostr-native marketing channel that
npubs can subscribe to and receive in-app.

Publishing (admins):
- Newsletter issues are published as NIP-23 long-form articles (kind 30023)
  tagged `skatespotsnews`, on the community relay.
- NewsletterComposer: admin-only dialog to write + publish an issue (title,
  slug, summary, cover image, Markdown body).

Subscribing (any npub):
- useNewsletterSubscription: per-user opt-in stored as a NIP-78 (kind 30078)
  marker, so it syncs across every device the user logs into.
- NewsletterSubscribeButton on the /newsletter page and issue reader.

Mailbox / notifications:
- New "News" tab in the NotificationBell showing all issues, with an unread
  badge for subscribers (tracked via a per-account last-read timestamp).
  Latest unread drop also surfaces in the "All" tab.
- Opening the bell or the /newsletter page clears the News badge.

Reading:
- useNewsletterIssues: queries admin-authored issues (author-scoped + tag +
  admin re-validation as the trust boundary against spam).
- LiveIssuesSection on /newsletter shows issue cards linking to a reader.
- NewsletterIssueView: naddr reader (wired into NIP19Page for kind 30023) with
  a small safe Markdown renderer, zap + subscribe CTAs.

Docs: NIP.md documents the 30023 issue schema, the 30078 subscription marker,
and the admin trust boundary.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 19:16:49 +07:00
shakespeare.diy
cdf8cee3a6 Update Sasquatch avatar to the correct mascot image
Point CREW_AGENT_AVATAR at the intended Sasquatch artwork. Since it's the
single source of truth, this updates the agent's image everywhere (panel
header/rows/empty state, crew page button, CrewForm toggle) and in the
agent's published kind-0 profile.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 18:59:05 +07:00
shakespeare.diy
878da94b7b Make Crew Agent genuinely crew-aware; swap bee emoji for Sasquatch avatar
Sasquatch now knows the crew it lives on. The agent is seeded with a rich
crew dossier — its own upcoming events, recent feed digest, roster size,
founder, open call, vibe, band, home spots, website/IG — and the system
prompt instructs it to answer crew-specific questions ("what's our next
jam?", "who runs this?", "what are we looking for?") directly from memory,
while still using CVM tools for wider-network discovery.

- useCrewAgentChat: expanded CrewContext + crew dossier + two-mode prompt
- CrewDetailPage: wires real crew events + feed digest into the agent ctx
- CrewAgentPanel: crew-specific suggestions, crew-aware copy
- Replaced 🐝 bee emoji with the Sasquatch mascot avatar everywhere
  (panel header/rows/empty state, crew page button, CrewForm toggle)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 18:55:54 +07:00
shakespeare.diy
0b786e546f Notification bell: drop the "Browse all crews" footer
A notification center should surface the user's own actionable items, not a
generic nav shortcut — and it showed on every tab (including Social) where it
made even less sense. Removed the footer entirely; the tabs now sit flush.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 18:38:33 +07:00
shakespeare.diy
c51d9f68d4 DM inbox: real unread indicators (was hardcoded to 0)
useConversations reported unreadCount: 0 for everything, so the inbox never
showed new messages. Add a lightweight, reliable unread signal:

- useDMRead: per-conversation, per-user last-read timestamps in localStorage
  (local-only; compares against the inner rumor's real created_at, not the
  fuzzed gift-wrap time, so it's accurate).
- DirectMessagesPage: a conversation is unread when its latest message is from
  the other person and newer than last-read — rendered as a red dot, bolder
  name, brighter preview text, and a subtle highlighted card.
- ConversationPage: opening a chat marks it read up to the newest message, so
  the dot clears on return (each page mounts fresh, so no stale state).

Scope: kept to the Messages page. Deliberately did NOT add a global nav badge,
which would require background-decrypting all gift wraps on every page — the
same heavy cost avoided for the crew bell.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 18:35:13 +07:00
shakespeare.diy
3833625bde Signup: require a display name (changeable later), remove skip
Per request, a display name is now mandatory when signing up:
- Removed the "Skip for now" button from the profile step — the only forward
  path is Save, which is disabled until a non-empty name is entered.
- Marked the field required (*), added helper copy ("Required — how you'll show
  up across SkateSpots. Change it anytime."), and a hint under the button.
- finishSignup guards against an empty/whitespace name (defense-in-depth for
  keyboard/Enter), trims the name before publishing kind 0.
- Reworded the step copy: name required, bio + avatar still optional.

Only affects the primary nsec signup dialog (also the endpoint the mnemonic
flow feeds into); the mnemonic dialog has no name step.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 18:32:08 +07:00
shakespeare.diy
39afc00b6e Crew chat: unread dot on the Chat tab (cheap, honest, no bell spam)
Instead of wiring encrypted crew chat into the global notification bell — which
would mean background-decrypting every crew's messages to power a badge (slow,
battery-hungry, unreliable with NIP-17's random timestamps) — add a subtle
unread dot to the crew's own Chat tab.

- useCrewChatUnread: member-only, single-crew, runs only while the crew page is
  open. Uses a pure metadata query (newest Cordn envelope by created_at, no
  decryption) vs. the local last-seen timestamp. For the built-in NIP-17
  transport (random timestamps, needs decryption) it deliberately returns false
  rather than fire false pings — the in-chat "New" divider already covers
  "what did I miss" once the tab is opened.
- Tabs made controlled; the dot is suppressed while the Chat tab is active, so
  it clears the moment you look, and is gone on the next visit (last-seen
  advances). No global notification noise.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 18:21:38 +07:00
shakespeare.diy
e843bd526a Cordn crew chat: ephemeral envelopes (coordinator can't see the sender)
Close the metadata gap: the outer kind-4497 envelope was signed with the
sender's real key, so a coordinator/relay could see exactly who was talking.

- buildEnvelope now signs each envelope with a fresh ephemeral key (NIP-59
  style) — the coordinator never learns the sender.
- Authorship moves inside the ciphertext: a new v2 sealed payload
  (`nip44-group-v2`) carries a fully-signed inner author-proof event (kind 4498)
  bound to the crew via an `a` tag. openGroupMessage verifies the signature,
  author, content match, and group binding — so authorship stays unforgeable and
  a proof can't be replayed into another crew.
- Transport trusts the verified v2 proof and surfaces the real sender; legacy
  v1 envelopes remain readable (outer-key binding) for back-compat.
- NIP.md updated with the v2 format + an honest note on the residual
  verbatim-replay caveat (full replay/forward-secrecy hardening lands with MLS).

Scope decision: kept to crew chat only. Regular 1:1/group DMs already use NIP-17
gift wrap (ephemeral keys, sealed per recipient) and have no coordinator, so
they need no change and stay idiot-proof.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 18:14:58 +07:00
shakespeare.diy
739469aa1d Crew chat: frame built-in E2E chat as the default, Cordn as honest opt-in
Decision (per Marmot/Concord/Cordn analysis): a semi-public, socially-verified
skate crew fits Concord-style shared-key chat, not MLS. The built-in NIP-17
giftwrap group chat is already the right answer and works with zero setup — so
stop implying a coordinator is required and stop over-claiming MLS guarantees
the code doesn't yet provide.

- CrewCoordinatorCard: default state now reassures that private E2E chat is
  already ON (no setup) for everyone, instead of prompting founders to "set up a
  coordinator". Founders get a small, honestly-labeled advanced link to the
  self-hosted Cordn option. Fixed the "E2E · MLS / forward secrecy" badge — the
  coordinator path is NIP-44 group encryption routed over ContextVM, not an MLS
  ratchet.
- CrewForm: the coordinator field is now clearly "experimental · self-host",
  states you don't need it, and links to the Cordn repo.
- NIP.md: reworded the crew-chat section + tag/profile descriptions to match the
  honest crypto seam (NIP-44 group today; MLS a future upgrade), so docs no
  longer claim forward secrecy that isn't shipped.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 18:02:53 +07:00
shakespeare.diy
e698e4880f Crew chat: subtle "New" divider for messages since last visit
A returning member couldn't tell new messages from old ones. Add the universal
unread-divider pattern (Slack/Discord-style) directly inside the chat:

- useCrewChatRead: per-crew, per-pubkey last-seen timestamp in localStorage
  (local-only, zero network cost, works for both NIP-17 and Cordn transports).
- CrewChat snapshots last-seen once per open so the divider stays put while
  reading, draws a minimal "New" line above the first message since last visit,
  and quietly advances last-seen to the newest rendered message.

No new fetches, no config, nothing for the user to manage — it just works.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 15:58:54 +07:00
shakespeare.diy
f37d0184d3 Collective join-requests: one-tap approve from the bell
Turn the collective join-request into a genuinely seamless flow instead of a
dead-end deep-link:

- New useApproveCollectiveJoinRequest: owner appends the requester to their own
  Lab-profile roster (re-signed, preserving all tags) and clears the request in
  one action — mirrors the crew founder's "Add to crew".
- CollectiveJoinRequestCard now resolves the collective and offers a single
  "Add to collective" tap, with graceful states for loading, already-a-member,
  and unresolvable-profile (never traps the owner).
- Removed the old ?manage=members deep-link (pointed at a read-only card).
- Requester-side copy now sets accurate expectations ("added in one tap").

Trust model unchanged: only the owner signs their profile event.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 15:49:49 +07:00
shakespeare.diy
314c8620f1 Lab collective join-requests → notification bell
Skaters can now request to join a Lab collective (studio/build team), mirroring
the crew join-request flow:

- New addressable kind 36387 (collective join-request), documented in NIP.md
- useCollectiveJoinRequests: send / incoming / "already requested" hooks
- Owner-only notification surfaced in the bell (All + Crews tabs) via
  CollectiveJoinRequestCard, reusing the synced dismissal set
- "Request to join this collective" button on collective Lab profiles
  (RequestToJoinCollective), hidden for the owner and existing members

Trust model preserved: only the owner can edit their own profile roster, so the
card links them to the member editor — no forged membership possible.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 15:41:03 +07:00
shakespeare.diy
64b91c0ae2 Mutual crew<->Lab link verification (trust badge) + polish
The crew<->collective links existed on both sides (crew.labRefs, profile.crewRef)
but were unverified one-sided claims. Now:
- CrossLinkCards resolve the other entity and show a green "Verified" badge only
  when BOTH sides link each other (crew lists the Lab profile AND the profile
  lists the crew). One-sided claims show a plain link — never false trust.
- LinkedCrewCard / LinkedLabCard take a selfCoord; CoachDetailPage and
  CrewDetailPage pass their own coordinate so the check works.
- CrewLinkField + LabLinkField now explain the mutual-verified behavior, so the
  feature is discoverable. Both fields already restrict to the user's OWN
  entities, so a verified link genuinely requires one owner to confirm both sides.
- Completed LabLinkField TYPE_LABEL for all crafts (artist/graffiti/tattoo/
  trainer/musician/judge no longer fall back to "Lab").

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 15:29:57 +07:00
shakespeare.diy
a89fca8e28 Lab: payment-readiness prompt on every provider form + drop downhill/longboard from judge
- New PaymentReadinessNote component: checks the logged-in user's Nostr profile
  (kind 0) for a Lightning address / Silent Payment and either confirms "you can
  get paid" or links them to Edit Profile to add one. Wired into CraftProfileForm
  (coach, filmer, editor, photographer, builder, artist, graffiti, tattoo,
  trainer, musician, judge) and the standalone coach/filmer/editor/photographer/
  builder forms, alongside the existing CLINK (noffer) field. Lightning address
  stays a single source of truth on kind 0 — never duplicated/stale per event.
- become-judge: removed the "Downhill / Longboard" event format (not part of the
  skateboarding activities this app covers).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 15:21:34 +07:00
shakespeare.diy
598362a196 Fix date-window chips overflowing off the phone screen
The chips (ANYTIME / TODAY / THIS WEEKEND / THIS WEEK / THIS MONTH) ran off the
right edge on mobile and got cut off. Now on phones they render as a 5-column
grid that fits the screen exactly — with compact labels (Any / Today / Wknd /
Week / Month) so nothing truncates. Desktop keeps the full-label flex row.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 15:11:30 +07:00
shakespeare.diy
8110efe821 Compress /events chrome on mobile so event cards reach above the fold
The mobile toolbar stacked 5 rows of controls (title, view tabs, upcoming/past
sub-tabs, search+city, date chips) before any card showed. Tightened for phones
without removing any feature or touching desktop:
- Toolbar vertical padding + section spacing reduced (py-2/space-y-2 on mobile)
- View tabs and Upcoming/Past sub-tabs use py-1.5/py-1 on mobile
- Date-window chips: smaller top margin + padding
- Results grid top margin reduced on mobile
- "N results" count line hidden on mobile (pure noise on a small screen)

All changes are md:-gated, so desktop is unchanged.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 15:08:45 +07:00
shakespeare.diy
d577c8e455 Prevent scheduling events in the past + full events-section audit
Audited the whole events flow end-to-end (create, hosting dashboard, discovery
grid, detail, RSVP, sponsors) and confirmed the wiring is coherent:
- Create → parse → display round-trips correctly for sponsors, tour/spot/crew/
  musician links, entry fee, and featured listings
- Sponsor parser safely skips shop-coordinate `sponsor` tags (no name at pos 2),
  so events and tour/shop sponsorships never collide
- Batch RSVP counts feed the grid; hosting dashboard + detail keep per-item counts

Fix surfaced by the audit: CreateEventPage let a host pick a start time in the
past, silently producing a dead event in the Past tab. Added a `min={today}` on
the date pickers and a submit guard (10-min grace) with a clear message.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 15:02:25 +07:00
shakespeare.diy
fbb70983cd Tighten /events header on mobile so cards sit higher above the fold
The filter toolbar was already sticky (stays put while cards scroll under it).
This reclaims the wasted vertical space above it on phones: smaller title,
reduced top padding + margins, and the subtitle is hidden on mobile. Desktop
layout unchanged.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 14:53:53 +07:00
shakespeare.diy
5a0e6d21a0 Hide visible scrollbar on mobile for a clean, app-like feel
On phones (max-width 767px) the gray scrollbar track is now hidden across the
app (html/body) — swipe-to-scroll still works, only the bar is gone. Desktop
keeps its custom 8px scrollbar unchanged.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 14:49:43 +07:00
shakespeare.diy
135fbaab40 Events discovery polish + real bug/perf fixes across the grid
UX (all participants):
- EventCard now shows a live status pill (In 2 days / HAPPENING NOW / ENDED) on
  both the full and compact variants — the at-a-glance signal was previously
  detail-page only
- EventCard shows a "Presented by <sponsor>" ribbon so paid sponsor placements
  get real visibility in the discovery feed (not just after a click)
- Added a bottom legibility gradient so overlays stay readable on any cover

FIX (crash risk): EventsPage getTrustScore did `trustScores?.map.get` — since
useBatchTrustScores returns a plain Map (no `.map`), this threw TypeError once
scores loaded. Now reads the Map directly.

PERF (N+1 query storm): rendering the grid fired one RSVP query PER card
(~100 queries on a full page). Added useBatchEventRsvps — a single #a-filtered
query for all event coordinates — and thread goingCount into EventCard as a prop.
Cards without the prop (detail page, hosting dashboard) keep their own query.

Removed now-dead getRelativeTime/relativeTime/isUpcoming (superseded by the
EventCountdown pill).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 14:47:02 +07:00
shakespeare.diy
854d18c322 Give admins moderator control over events + fix a spoofing hole
Admins (the two pubkeys in src/lib/admins.ts) can now moderate any event:
- Post authoritative Event Updates (incl. cancel/reschedule) on events they don't own,
  clearly labelled "Admin" and marked with an ["admin","true"] tag
- Take down any event (already wired via DeleteEntityDialog + isAdminPubkey)

SECURITY FIX: kind-7103 Event Updates are now only trusted from the event's host
(pubkey in the `a` coordinate) or a SkateSpots admin. Previously ANY npub could
publish a fake "CANCELLED" update — spoofing the detail-page banner and spamming
RSVPers' notification bells. Enforced in both parseEventUpdate() and
useActivityNotifications.

Note: admins intentionally CANNOT open the edit form on others' events — an
addressable event (31923) can only be replaced by the key that signed it; editing
under a different key would fork it, not edit it. Moderation is done via
authoritative updates + takedown instead.

Docs: NIP.md updated with the host/admin trust rule and the admin marker tag.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 14:40:17 +07:00
shakespeare.diy
25c57cbb2f Fix event UX correctness: live countdown, cancellation banner, NIP-52 tag cleanup
- FIX: EventCountdown label never updated (memo ignored the interval tick) — now recomputes every 30s for both EventCountdown and useEventStatus
- Add loud cancelled / rescheduled banner on EventDetailPage derived from host updates; close RSVP + dim hero when cancelled so attendees don't show up to a dead jam
- Clean up non-standard NIP-52 `a` tags (['a', coord, '', ''] → ['a', coord]) in RSVP and host-update publishing
- Correct misleading kind comment in useEventRsvps

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 14:33:28 +07:00
shakespeare.diy
29d09eb6ec Add rich Open Graph / Twitter share metadata + canonical share link to event page
- Per-event og:title/description/image/url + twitter:summary_large_image via useSeoMeta
- Share description composes date + location + blurb; falls back to site card image
- handleShare now uses the canonical /#/event/... URL, richer text, and toast feedback on clipboard copy

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 12:46:45 +07:00
shakespeare.diy
87c81a63c9 Event detail hero redesign + live countdown + sponsors
- Immersive EventDetailPage hero: full-bleed cover with legibility gradient,
  bold uppercase title, type pills, host/music-act chips, and a 3-up meta bar
  (date block · time · where). Past events render dimmed.
- EventCountdown: live status pill — "IN 3 DAYS" / "IN 2H 14M" /
  "HAPPENING NOW" (pulsing) / "ENDED", refreshing every 30s.
- Actions (edit/calendar/share) moved into a clean glass overlay on the cover.
- Event sponsors: EventSponsor type + parse/serialize (mirrors TourSponsor),
  EventSponsors "Presented by" strip (title sponsor hero row + logo grid),
  sponsor editor on CreateEventPage, and sponsor preservation on edit.
- Document event sponsor tag in NIP.md.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 12:04:07 +07:00
shakespeare.diy
78f4a6d09a Host tools: attendee list w/ trust, event updates, edit-alerts, Hosting tab
- AttendeeList: tabbed Going/Interested with faces + names + a "who you trust"
  signal (reuses batch trust scores); replaces the plain avatar stack
- Event Updates (kind 7103): usePostEventUpdate + useEventUpdates; host-only
  composer with update/reschedule/relocation/cancelled categories that p-tags
  every RSVPer so they get notified; public feed on the event page
- EditEventPage: auto-post a reschedule/relocation update to notify attendees
  when the host changes time or location
- Activity bell: new 'event-update' notification type for RSVPers
- EventsPage: "Hosting" tab (logged-in) with a HostingDashboard listing the
  user's upcoming/past events, live RSVP counts, and quick edit/update actions
- Document Event Update kind 7103 in NIP.md

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 11:25:57 +07:00
shakespeare.diy
7439b980bb Events Phase 1: NIP-52 RSVPs, attendee stacks, date filter, host notifications
- Adopt NIP-52 RSVP (kind 31925): parseEventRsvp + RsvpStatus types
- useEventRsvps (counts by status, attendee pubkeys, my status) + useRsvp
  mutation that replaces the user's prior RSVP via a stable d-tag
- RsvpButtons (Going / Interested / Can't make it) + AttendeeStack avatar row
- EventDetailPage: "Who's going" card with RSVP control + attendee avatars
- EventCard: going count on both compact and full cards
- EventsPage: quick date-window chips (Today / Weekend / Week / Month)
- Activity bell: host RSVP notifications (new 'rsvp' activity type)
- Document RSVP schema in NIP.md

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 10:47:57 +07:00
shakespeare.diy
453adc4604 Bring desktop/tablet "More" menu to parity with mobile burger
The desktop "More" dropdown was only populated from the customizable nav
catalog, so directory/account destinations that live only in the mobile
burger menu (notably Skaters) never appeared. Add Skaters, View Profile
(logged in), and Settings to the desktop More menu so nothing is lost on
larger screens.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 10:29:54 +07:00
shakespeare.diy
c34f194d8f Polish notifications: full activity center (social + crews), not just crews
- New useActivityNotifications: single combined #p query classifies reactions,
  reposts, zaps, mentions and replies referencing the user; parses zap sats
- New useNotificationsRead: per-account last-read timestamp for unread badge
- New ActivityNotificationCard: actor avatar + type badge, verb, snippet, links
  to the actor's profile
- Rewrite NotificationBell into a tabbed activity center (All / Social / Crews)
  with unread tracking, mark-all-read, and the existing crew invites/requests

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 10:21:15 +07:00
shakespeare.diy
35915557bb Add in-app routing (foot/bike/moto/car) via OSRM — no external maps handoff
- New src/lib/routing.ts: keyless OSRM client, travel modes, distance/skate-time formatters
- New RouteDialog: self-contained Leaflet map drawing the route from the user's
  location to a spot/shop/event, with a mode toggle and distance/time summary
- Wire the "Directions" buttons on Spot/Shop/Event detail pages to route in-app
  instead of opening Google/Apple Maps

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 10:17:06 +07:00
shakespeare.diy
f4bca7b736 Admins can endorse join-requests; founder ratifies (kind 9087)
Closes the admin/founder asymmetry without breaking the trust model: only the
founder authors the crew event, so only they add members — but an admin can now
publicly *vouch* for a join-request, and the founder ratifies with one click.

Protocol (SkateSpots-native kind 9087, regular event):
- e-tags the join-request, a-tags the crew, p-tags the requester (role
  "requester") and the founder (so it hits the founder's bell).
- Clients keep only endorsements authored by a real admin of the endorsed crew
  and dedupe one-per-admin-per-request (no credibility inflation).
- New parseCrewEndorsement + CrewEndorsement type; documented in NIP.md.

Hooks:
- useEndorseJoinRequest (admin publishes an endorsement, admin-gated).
- useJoinRequestEndorsements: batch-fetch endorsements for all visible requests
  in ONE #e query, grouped per request, admin-verified per crew.
- useCrewNotifications attaches endorsements to each request entry.

UX (JoinRequestCard):
- Founder: primary becomes "Approve & add" when vouched, with a vouch strip
  (stacked admin avatars + "Vouched by Rider +1 more") as social proof.
- Admin: the old dead-end "Only the founder can add members" becomes a real
  "Vouch for them" action, then a "Vouched — waiting on the founder" state,
  plus a one-line explainer so the path isn't a mystery.
- Endorsements enrich the existing request card (no duplicate notification).

Build clean, no console errors. Not deployed.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 01:40:04 +07:00
shakespeare.diy
0f2215f46c Sync crew-notification dismissals across devices (private NIP-78)
Fixes the caveat that dismissing a handled invite/join-request on one device
left it un-dismissed on another (it was per-device localStorage).

- New useCrewDismissals hook: stores the dismissed-id set as a single private,
  addressable NIP-78 event (kind 30078, d=skatespots:crew-notif-dismissals)
  with a NIP-44 payload encrypted to the user's own key. One list covers both
  invites and join-requests.
- useDismissCrewNotification: optimistic local update + best-effort encrypted
  publish, so dismissals feel instant and sync across every device the user
  logs in from.
- Graceful degradation: always resolves the UNION of the synced list and a
  local mirror, so it still works offline and on signers without NIP-44 (they
  just don't sync) — no regression from the old behavior.
- Rewired useCrewInvites (accept/decline/approve/dismiss) to the synced set;
  removed the two localStorage-only dismissal helpers. Filtering moved into
  useCrewNotifications so both invite + request lists share one source of truth.
- Documented the kind-30078 dismissal event in NIP.md.

Build clean, no console errors. Not deployed.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 01:32:04 +07:00
shakespeare.diy
042e4af8ed Expand crew roles to the full Lab craft roster
Crew roles previously covered only filmer/photographer/editor/artist plus
manager/og. Bring in the rest of the Lab professions so a crew can label any
member by the craft they bring:

- Added: coach, builder, graffiti, tattoo, trainer, musician, judge —
  mirroring the Lab profession kinds 35001–35012 (with a `labKind` link in
  CREW_ROLE_META for future cross-linking to Lab profiles).
- Kept the crew-specific manager + og, and preserved existing slugs so no
  stored crew data breaks.
- Reordered for display: media crafts → Lab crafts → crew-specific.
- Aligned emoji with the Lab (📸 🎓 🔨 🖌️ 💉 💪 🎵 ⚖️).

All consumers (CrewForm role chips, CrewLeadership, CollectiveMembers,
parseCrewRoles) pick these up automatically via CREW_ROLE_ORDER / _META.
Updated the crew-role docs in NIP.md.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 01:07:44 +07:00
shakespeare.diy
e2a0184260 Polish create-crew wizard: live feedback, smart stepper, member preview
Wizard chrome:
- Replace the flat progress bar with a proper numbered stepper — icons per
  step, a filled/checkmark state when a step has content, past-step checks,
  and clickable dots (forward jumps still gated on a name).
- Direction-aware step transitions (slide from right going forward, left
  going back) with fade-in.
- Sticky footer gets arrow icons, a shadow, a "ready to roll / add a name"
  hint on the final step, and Create Crew disabled until the name is valid.
- Enter / form-submit before the last step now advances instead of publishing
  a half-filled crew (edit mode unaffected).

Field-level feedback:
- Crew name: inline "Looks good" + check icon when valid, teal border, helper
  line, 100-char cap.
- About: live character counter (turns amber near the 3000 limit).

Member input:
- Live preview chip (avatar + name + npub) that resolves the typed
  npub/nprofile/hex BEFORE adding, so founders confirm the right skater.
- Add button disabled until a valid, not-yet-added key is present; amber hint
  when the text isn't a key yet; "Added" state when already on the roster.
- Extracted a shared decodePubkeyInput helper (also now accepts nostr:nprofile).

Crew Agent:
- Honest beta framing (browser-run, free-tier AI, occasionally slow) and an
  on-state confirmation line when enabled.

No publish/handleSubmit behavior changed; all edits are UX/IA only.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 00:59:30 +07:00
shakespeare.diy
5716870863 Redesign create-crew into a guided, idiotproof wizard
Audit finding: the crew form was one 1,100-line endless scroll with ~15
sections in a punishing order (logo dead-last, MLS chat coordinator sitting
inline with the crew name), only one validation gate, and no sense of
progress or what's optional.

Fix — split the flow by intent:
- Create mode: a guided 4-step wizard with a sticky progress bar, per-step
  hints, Back/Continue + "Skip to end", and a sticky footer nav.
    1. The Basics   — logo, name, about, home turf (all you actually need)
    2. Identity & Vibe — vibe tags, crew type
    3. Squad & Spots  — members/roles, home spots, open call
    4. Extras         — treasury, video, sponsor, links, band, NIP-05,
                        agent, and the advanced chat coordinator (reframed
                        in plain language, clearly skippable)
  Only the crew name is gated, and only at step 1. Clickable step dots allow
  jumping back freely.
- Edit mode: unchanged UX intent — renders every section flat on one page
  (power users want it all at once).

All field state and the entire publish/handleSubmit logic are preserved
verbatim; sections were extracted into composable blocks with zero behavior
change. Softened the coordinator copy and the map-pin section so a first-timer
isn't scared off.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 00:44:59 +07:00
shakespeare.diy
a2c34800ce Add sponsor picker + notifiable crew join-requests
Part A — Sponsor picker:
- CrewSponsorPicker searches the shop directory and emits a 30017 coordinate
  so founders select a sponsoring shop instead of pasting a raw coordinate.
- Wired into CrewForm, replacing the raw coordinate input.

Part B — Notifiable join-requests (kind 31969):
- New addressable join-request event p-tagged to founder + admins, making the
  request→approve direction symmetric with invites (surfaced in the bell, not
  buried in a DM inbox).
- useCrewInvites: useSendJoinRequest, useIncomingJoinRequests,
  useApproveJoinRequest (founder adds member), useDismissJoinRequest (local).
- CrewRequestToJoin now publishes the structured event.
- JoinRequestCard (Add to crew / Dismiss) wired into NotificationBell and
  counted in the unread badge.
- Documented kind 31969 in NIP.md alongside invites.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 00:23:55 +07:00
shakespeare.diy
50d8d9e614 Wire live Cordn encrypted crew chat over the coordinator (ContextVM)
The crew coordinator is no longer just a pointer — when a crew configures one,
the Chat tab runs through it for real.

- src/lib/cordn: clean transport/crypto split so an MLS (ts-mls) upgrade is a
  drop-in swap of the crypto layer only.
  - types.ts: CordnMessage/CordnEndpoint + envelope kind 4497 + app tag.
  - crypto.ts: sealGroupMessage/openGroupMessage — real NIP-44 group encryption
    (per-recipient sealing), the exact seam MLS encrypt-to-epoch slots into.
  - client.ts: buildEnvelope/publishEnvelope/fetchGroupMessages with per-group
    cursor ordering and ciphertext→author sender binding (anti-spoof).
  - endpoint.ts: resolve a crew's `coordinator` to a usable relay endpoint
    (wss/ws direct; https falls back to the ContextVM relay).
- useCordnChat: fetch-then-poll hook mirroring the NIP-17 chat API.
- CrewChat: uses Cordn when the crew has a coordinator, NIP-17 fallback
  otherwise; header badge shows the active transport (Cordn vs Private).
- CrewCoordinatorCard: reflects that private chat is live.
- NIP.md: document envelope kind 4497, delivery (fetch-then-poll + cursors),
  the nip44-group sealed content, and the honest crypto→MLS seam.

Real encryption, real coordinator routing, non-breaking (fallback intact),
with the MLS ratchet as a clearly-marked next step.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 00:15:22 +07:00
shakespeare.diy
cbd30fee00 Add skate roles to the crew roster (filmer, photographer, etc.)
A skate team is more than "members" — the filmer, photographer and editor are
core functions. Crews can now assign optional, non-pretentious skate roles:

- Types: CrewRole (filmer/photographer/editor/artist/manager/og) + CREW_ROLE_META
  labels/emoji, `crew-role` tag parser, and `crewRoles` on SkateCrew. Roles are
  additive to membership and grant NO powers (only `admin` does); a member can
  hold roles AND be an admin. Only honoured for founder/listed members.
- CollectiveMembers: optional rolesByPubkey → renders 🎥 Filmer / 📷 Photographer
  flex chips next to each member.
- CrewLeadership: leader + captains show their skate-role chips.
- CrewForm: per-member role toggles + a founder ("Your roles") picker; publishes
  `crew-role` tags scoped to the roster.
- CrewDetailPage: builds rolesByPubkey and threads it to members + leadership.
- NIP.md: document the `crew-role` tag + example.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 00:07:30 +07:00
shakespeare.diy
91045e17ae Add "The Part" — crew video/montage (YouTube + Instagram)
A skate crew is defined by its footage. Crews can now feature a headline video:
- videoEmbed.ts: safe parser accepting ONLY YouTube and Instagram URLs (full
  parts on YT, clips on IG). Extracts video id / shortcode; rejects everything
  else so we never render an untrusted iframe. No native/self-hosted video.
- CrewVideoSection ("The Part"): cinematic section — YouTube click-to-load
  facade (poster + play button, no heavy iframe/cookies until play) and an
  Instagram permalink embed, framed with skate language.
- SkateCrew gains `videoUrl` (tag `video`), validated through the embed parser.
- CrewForm: "The Part" input with live source detection (YouTube/Instagram) and
  validation; publishes the `video` tag.
- Crews grid: crews with footage flex a "▶ Part" badge as a discovery signal.
- NIP.md: document the `video` tag (YouTube/Instagram only) + example.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 23:59:50 +07:00
shakespeare.diy
4f3339650d Add crew roles & relationships (Phase 3: sponsor + Cordn coordinator)
- Extend SkateCrew with `sponsorRef` (sponsoring shop, NIP-15 stall coord) and
  `coordinator` (Cordn/MLS encrypted-chat Delivery Service pointer), plus parser.
- CrewSponsorBanner: "Sponsored by [Shop]" banner that resolves and links the
  backing shop; renders nothing if the shop can't be resolved.
- CrewCoordinatorCard: surfaces the crew's private-chat coordinator (E2E · MLS),
  explains what the coordinator can/can't see, shows the operator, and prompts
  the leader to set one up when absent — the UI foundation for Cordn chat.
- Wire the sponsor banner (under hero) and coordinator card (sidebar) into
  CrewDetailPage.
- CrewForm: add "Sponsored by" and "Private Chat Coordinator" inputs with
  validation; publish `sponsor` and `coordinator` tags.
- NIP.md: document the `sponsor` and `coordinator` tags, the structured invite
  kind 36229 (with accept/decline flow), and the Cordn/MLS private-chat model
  and why it's the chosen fit for crews.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 23:52:33 +07:00
shakespeare.diy
8a384c79d2 Redesign crew detail + crews page (Phase 2: visual)
- New CrewHero: immersive full-bleed banner with per-crew accent glow, club-crest
  logo overlapping the banner, member-count crest, big team name with neon shadow,
  reputation crown + live activity pulse, focus/verification chips, "Led by"
  team-leader inline, vibes and bio. Actions are passed in from the page.
- New CrewLeadership card: frames the founder as "Team Leader" and admins as
  "Captains" with role crests, verified ticks and quick-DM — establishing the
  crew hierarchy and laying groundwork for coordinator roles.
- Rewire CrewDetailPage to use CrewHero + CrewLeadership; wire live activity via
  useCrewsActivity and confirmed-member/admin sets. Removed the old flat hero
  card and now-unused imports.
- Upgrade CrewsPage header into a proper hero band with gradient, glow and
  stat pills.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 23:07:27 +07:00
shakespeare.diy
e8c1fbc71a Add crew invitation & notification system (Phase 1)
- Upgrade CrewInviteDialog with a "Send invite" tab: invite a specific npub
  with live invitee preview, optional note, and founder-only "invite as admin"
  toggle. Preserves the shareable-link flow on a second tab.
- Add NotificationBell to desktop + mobile headers: unread badge, popover with
  incoming crew invites (accept/decline) and pending-confirmation crews
  (confirm with public/crew-only visibility).
- Wire useSendCrewInvite / useMyCrewInvites / accept / decline hooks and the
  useCrewNotifications aggregator into the UI.
- Membership stays unforgeable (founder p-tag + invitee NIP-32 attestation).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 22:59:34 +07:00
shakespeare.diy
6ea96171ad Surface Sponsors & Support up top on the profile
- Move sponsors from a buried sidebar card to a prominent full-width "Backed by"
  bar right under the stats strip, above the About/content grid
- Redesigned as a horizontal pill row with an Award badge + gradient/glow accent,
  so reppin' brands reads as a badge of pride
- Removed the duplicate sidebar Sponsors card

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 22:38:36 +07:00
shakespeare.diy
2bb0f98f45 Redesign profile stats strip into a balanced, scannable grid
- Even 2x2 (mobile) / 1x4 (desktop) grid instead of a ragged flex-wrap where
  "Gear" dropped to its own row and "Member since" floated awkwardly
- Each stat now has an icon badge + prominent count + label, so the number reads
  as a number (previously the tiny colored count looked like a status dot)
- Zero-value stats render in a muted, inert state — a brand-new profile now reads
  as "nothing yet" rather than a wall of dead grey zeros
- Moved "Zaps received" + "Member since" into a clean secondary meta line below,
  keeping the primary grid focused on the four core contributions

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 22:29:10 +07:00
shakespeare.diy
9958020776 Make profile stat pills clickable — jump to their section
- StatPill now accepts an onClick and renders as a hover-lift button when it has
  a non-zero value (inert div when empty), with clear affordance
- Spots / Shops / Crews / Gear pills smooth-scroll to their matching section
  (added ids + scroll-mt-24 offset so headings aren't hidden under sticky nav)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 22:23:22 +07:00
shakespeare.diy
8ba54273b7 ShareProfileDialog: no horizontal scroll + canonical share URL
- Dialog now sizes to the viewport (w-[calc(100vw-2rem)] max-w-sm) with
  overflow-hidden and min-w-0 children so the QR + URL never force lateral scroll
- Slightly smaller QR (48/52) to fit comfortably in the narrow dialog
- Share links/QRs now always anchor to https://skate-spots.org instead of
  window.location.origin, which in the editor resolves to the sandbox iframe host
  (the *.iframe.diy URL you saw)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 22:16:09 +07:00
shakespeare.diy
1b57f79cba Fix blank QR in ShareProfileDialog — import qrcode lazily
Top-level `import QRCode from 'qrcode'` crashes on its canvas dependency at
module load, leaving the QR blank. Switch to the shared generateQRCode helper
which imports qrcode lazily (dynamic import), matching every other working QR
in the app. Also reset the QR while regenerating and log failures.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 22:12:45 +07:00
shakespeare.diy
deb8fe1995 Profile polish: NIP-05 field UX, share+QR, at-a-glance stats, cleaner empty states
NIP-05 (the field you couldn't find — it existed but was a bare input):
- Promote to its own prominent, self-explaining row (out of the cramped grid)
- Live verification: checks the handle resolves back to your own key, with checking/verified/mismatch/unreachable states, an inline status icon, plain-language copy, and a "How?" help link

Share + QR:
- New ShareProfileDialog: QR of the profile URL (great for IRL/stickers), copy-link, and native share-sheet fallback
- "Share" button added to every profile header

At-a-glance stats:
- Compact stat strip near the top (Spots / Shops / Crews / Gear / Zaps + "Member since")
- Removed the now-redundant sidebar "Contributions" card

Cleaner empty states:
- Spots/Shops sections no longer show dead "nothing here" cards on other people's profiles — hidden entirely when empty
- Own empty Spots state is now an actionable nudge linking to /create-spot

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 22:08:49 +07:00
shakespeare.diy
a6c90941d2 Ensure Become-Skater ↔ Profile field consistency
Fields collected on the Become-Skater form were being dropped on the profile:
- Instagram: now rendered in the profile Info card (normalizes @handle / full URL, links to instagram.com), matching where website/lightning live
- Location (city/area): now shown as "Based in" in the skate-layer stats row
- hasSkateLayer now also triggers on location/instagram so the skate card renders even when those are the only fields filled

All other fields (photo from kind-0, bio, styles, stance, level/custom level, years, home spot + mapped link, website) already round-trip consistently between the two pages.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 21:56:28 +07:00
shakespeare.diy
dbae3a1501 Add soft NIP-05 verification for skaters (open directory + "Verified" filter)
Keeps the skater directory fully open — NIP-05 is a trust signal, not a gate:
- Extract reusable NIP-05 verification into lib/nip05.ts (parse, fetch well-known, verify against expected pubkey); refactor useCrewNip05 to use it
- New useSkaterVerifications hook: batch-fetches kind-0 metadata for all skaters and verifies each nip05 resolves to its own pubkey, returning a Set of verified pubkeys
- SkatersPage: add a "Verified (N)" toggle (disabled when none verified) that filters to genuinely verified skaters; show ✓ badge only for truly-verified skaters; verified-aware empty state
- NIP19Page profile: nip05 line now reflects real verification (lime ShieldCheck + "Verified" pill when the handle resolves to the key, muted grey otherwise)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 21:51:11 +07:00
shakespeare.diy
bb6245e53c Skater profile: browse real map spots for home spot + always use kind-0 profile pic
Home spot:
- Add "Browse spots" button opening a searchable dialog of real mapped spots (useSkateSpots)
- Picking a spot stores its title + an `a` (30818:pubkey:d) coordinate; shows a linked-spot chip with unlink
- Profile page renders a linked home spot as a link to /spot/:pubkey/:identifier
- Parse homeSpotRef from the `a` tag; document in NIP.md

Profile pic consistency:
- Remove the skate-specific image override/upload — the skater card now always
  uses the kind-0 profile picture so both sections show the identical photo
- "Profile Pic" section is read-only with a "Change profile photo" link to settings
- Drop the `image` tag from published skater events; note the change in NIP.md

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 21:44:12 +07:00
shakespeare.diy
4c9839eb45 Rename "Action shot" to "Profile Pic" and add custom Level option for skaters
- Become-Skater form: photo section relabeled "Profile Pic"
- Level picker now includes a "Custom" toggle with a free-text input (≤40 chars) for labels like "OG", "Weekend Warrior", "Retired"
- Store custom level as a `level_custom` tag (mutually exclusive with preset `level`)
- Render custom level everywhere presets show: profile (NIP19Page), SkatersPage cards, SkaterDirectoryCard
- Document `level_custom` tag in NIP.md

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 21:34:59 +07:00
shakespeare.diy
3ef11eda61 Refine Lab banner copy, stance to Regular/Goofy, full profile carry-over chips on Become-Skater, remove Back to Leaderboard from profile
- Lab "Just a skater?" banner: drop redundant "→ join the directory" trailer (arrow icon already present)
- Skater stances distinct emojis (🛹 Regular / 🤙 Goofy); already limited to Regular & Goofy
- Become-Skater: fix missing ShieldCheck/Zap imports; show ALL carried-over kind-0 fields (photo, name, bio, website, nip05, zaps) as read-only chips
- Profile (NIP19Page): remove hardcoded "Back to Leaderboard" link and unused ArrowLeft import

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 21:28:55 +07:00
shakespeare.diy
67d8a24d73 Fuse skater profile (35000) with Nostr identity (kind 0)
Make the skater layer and the universal Nostr profile feel like one person,
with kind-0 as the single source of truth for shared fields.

1. Public profile (NIP19Page): render the skate layer inline — stance, level,
   styles, years, home spot as a branded card right under the About. Own-profile
   nudge ("Add your skate details") when no 35000 exists. Website in the sidebar
   now falls back to the skater profile when kind-0 has none.

2. Become-Skater: prefill bio/website from kind-0 and default the photo to the
   kind-0 avatar (with "using your profile photo" affordance). Store shared
   fields ONLY as overrides — matching values are dropped so kind-0 stays
   authoritative and nothing goes stale. "From your profile" hints + a
   linked-to-your-profile banner make the relationship explicit.

3. Fallback-on-render for website/image/bio wherever the skater field is empty.

4. Bidirectional cross-links: new SkaterDirectoryCard in Settings > Profile
   shows directory status (level + style chips) linking to edit, or nudges the
   user to join; Become-Skater links back to full-profile editing.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 21:11:36 +07:00
shakespeare.diy
b42c234709 Add Skater profiles (kind 35000) + /skaters directory
- New app-native Skater Profile kind 35000 with `skateskater` marker tag,
  the anti-spam boundary that keeps generic kind-0 Nostr profiles out of the
  directory (queried only from the community relay).
- Skater type, stance/style/level constants, and parser in types.ts.
- useSkaters + useUserSkaterProfile hooks.
- /skaters directory page: immersive hero, live count, search, style filter
  chips, ranked skater grid with NIP-05 verification badges and lazy author
  metadata.
- /become-skater opt-in flow (bio, photo, styles, stance, level, years, home
  spot, location w/ geohash, socials).
- Wire "Skaters" into the mobile nav and a cross-link banner in The Lab so
  plain skaters finally have a home.
- Document kind 35000 in NIP.md.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 20:58:51 +07:00
shakespeare.diy
9599adaf0d Redesign profile edit form UI/UX + per-line sponsors
- Restructure EditProfileForm into clean, titled sections (Identity,
  Bitcoin & Payments, Skate Profile, Account) with gradient icon
  headers, consistent spacing, and a reusable FieldRow with inline
  icon + validation status.
- Sponsors is now a dynamic per-line list (add/remove rows, Enter to
  add) instead of a textarea; stored newline-separated. The profile
  renders each sponsor as its own badge.
- Lightning address placeholder is now generic (you@yourdomain.com) —
  no specific wallet names anywhere.
- Nicer image upload: click-the-thumbnail-to-upload with hover overlay,
  round avatar preview / wide banner preview.
- Sticky Save bar at the bottom of the form.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 20:06:32 +07:00
shakespeare.diy
11c67fcf22 Profile: add Lightning, Silent Payment, Sponsors & Cordn fields
Editable in EditProfileForm and displayed on the profile:
- Bitcoin Lightning Address (lud16) — now editable in-form, shown with a
  labeled Lightning row in the Info card.
- Bitcoin Silent Payment Address (BIP-352, custom `bitcoin_silent_payment`)
  — validated (sp1…), shown as a copyable, truncated row.
- Sponsors & Support (custom `sponsors`) — freeform highlight, rendered in
  a dedicated pink "Sponsors & Support" sidebar card.
- Cordn contact link (custom `cordn`) — private MLS messaging link
  (cordn.net), shown as a "Message privately" row in the Info card.

Crews and the CLINK noffer tip button were already present, so they are
left as-is. Added isValidSilentPayment / isValidLightningAddress
validators and documented the custom kind-0 fields in NIP.md.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 20:00:21 +07:00
shakespeare.diy
b3a49499bb 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>
2026-09-02 19:41:45 +07:00
shakespeare.diy
32a3cbca88 Public Stash items: "The Quiver" showcase + marketplace hybrid
Previously the "Public" visibility on Stash items did nothing — the
whole Stash is a single NIP-44 encrypted blob, so nobody could ever see
a "public" item. This makes Public actually mean public.

Model (hybrid A + marketplace):
- Public items are now ALSO published as separate unencrypted NIP-99
  kind 30402 events tagged `t: stash-showcase`, keyed by `stash-<id>`.
- Public + not-for-sale → showcase-only (`showcase: true`, price 0):
  shows on the profile "Quiver", filtered OUT of the sellable market.
- Public + for sale/trade → full listing: shows on the profile Quiver
  (badged For Sale/Trade) AND flows into the marketplace.
- Going private or deleting an item publishes a NIP-09 deletion for its
  public event, keeping everything consistent.

Changes:
- New hook useStashShowcase (sync/unpublish + useUserQuiver query).
- StashPage wires add/edit/remove through sync handlers; honest
  visibility copy in add + edit dialogs.
- Profile page (NIP19Page) renders a new "The Quiver" grid with
  For Sale/Trade badges + prices, and a Gear count in Contributions.
- Marketplace hooks skip showcase-only events so they don't clutter
  buy/sell listings.
- NIP.md documents the showcase event schema.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 19:33:34 +07:00
shakespeare.diy
889f6311ed Mobile bottom nav: Profile tab now opens your profile, not Settings
The avatar "Profile" tab in the mobile bottom bar linked to /settings,
so tapping your own face took you to Settings instead of your public
profile. It now routes to /:npub (View Profile) when logged in, and
still falls back to /settings when logged out (where login lives).
Long-press to customize the nav is preserved.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 19:23:56 +07:00
shakespeare.diy
9086123240 Fix messy Stash header/toolbar layout on mobile
- Let the title badges (Encrypted · Syncs everywhere · sats) wrap
  gracefully instead of colliding with the toolbar controls
- Hide the list/grid view toggle on the empty state (nothing to toggle)
  so it no longer clutters the space next to the Add button
- Give the title row room to grow on mobile (min-h-14 + padding)
  instead of forcing everything into a fixed 56px row
- Make the search input flex-fill and shrink the category select on
  small screens so the filter bar no longer overflows
- Add aria-labels to the view-toggle buttons

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-02 19:17:40 +07:00
shakespeare.diy
955d5902d5 Add RK69 DIY Skatepark to Supporters / Friends
Links to https://www.instagram.com/rk69diyskatepark, placed next to SNT4EVER.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-08-31 19:42:16 +07:00
shakespeare.diy
a0c8493502 Sasquatch: open the panel on the LEFT again on desktop
- Panel now anchors md:bottom-16 md:left-4 (was bottom-right) so it opens on
  the left as before, and sits ABOVE the desktop footer so it never covers the
  "Made with..." row or footer links.
- Tightened max-height to clear the top nav too (never slides under the menu).
- Bubble stays bottom-right on desktop for discoverability (clear of footer).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-08-27 16:28:07 +07:00
shakespeare.diy
407ba40a33 Sasquatch: full public-data coverage (all Lab crafts + shops + tours)
- findCoaches now covers EVERY Lab craft (coach, filmer, editor, photographer,
  builder, artist, graffiti, tattoo, trainer, musician, judge) via role param;
  reads skills from `t` tags and strips self-tags.
- New findShops (kind 30017, t=skateshop) and findTours (kind 30823) tools.
- getCommunityStats counts all crafts individually + lab_total + shops + tours,
  and filters spots to t=skatespot (drops foreign kind-30818 events).
- Wired new tools + expanded role vocabulary into the Sasquatch brain: system
  prompt, intent parser, keyword detector, and both dispatch/format paths.
- Privacy preserved: the public CVM data layer never queries the encrypted
  Stash (kind 30001) — verified no 30001/decrypt references. Personal 'my
  stash' stays login+Pro gated and decrypts locally with the user's own key.
- Normalized useCVMClient.ts to clean UTF-8 (comment glyphs only; no code change)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-08-27 16:14:20 +07:00