Add a purposeful subset of Amethyst-style relay categories (not a full clone):
- App-local 'Local relays' (never published; added to reads+writes)
- App-local 'Blocked relays' (excluded from every read/write)
- src/lib/relays.ts: resolveRelays()/useRelays() + URL normalisation
- NostrProvider req/event routers go through resolveRelays (blocklist applied)
- RelayListManager: Local/Blocked editors; NIP-65 save now preserves the new
lists instead of overwriting relayMetadata
Deliberately omitted (documented in AGENTS.md): KeyPackage (10051, no MLS
consumer) and proxy/broadcast/private-home (no such discovery model here).
Follow-up noted: a few hooks still read config.relayMetadata.relays directly.
Real, oracle-free escrow built on Cashu P2PK with a locktime + refund key:
buyer locks funds to the seller; seller redeems on delivery; the buyer can
reclaim after the deadline. Locked (not plain) tokens are what make the
cashu_token on a public 31872 safe.
- src/lib/cashuEscrow.ts: pubkey normalisation (x-only -> compressed), P2PK
policy builder, offline token inspection (amount/mint/locktime/refund), and
an injectable createEscrowToken mint flow (quote -> pay -> mintProofs{p2pk})
- 15 unit tests incl. the mint orchestration against a fake wallet
- Protocol: advertise a 'cashu_escrow' pubkey in kind 0 (NIP.md); token rides
the existing 31872 cashu_token tag
- ESCROW.md: design, threat model, status, limitations
- AGENTS.md: project map + docs index updated
Wallet UI (cashu-ts + payment rail + profile publish) is native-only and still
needs on-device verification.
Symptom: in the long-running dev server some lazy routes (/settings, /stash,
/collections, /stats) hit the route error boundary ('This page slammed') while
others worked. Console: 'Failed to fetch dynamically imported module'; network:
504 for node_modules/.vite/deps/*.js. Vite's dep-optimizer cache had gone
missing after the server re-optimized on a lockfile change, leaving stale ?v=
hashes.
- Add lazyWithRetry: on a chunk-load failure, retry once, then do a single
full reload per session (avoids reload loops); clears the flag on success
- Use it for all 53 lazy routes in AppRouter
- Document the dev dep-cache gotcha + fix in AGENTS.md
Data-driven pass (adds rollup-plugin-visualizer behind ANALYZE=1):
- MapPage was statically imported by the router, dragging the map route AND the
IndexedDB offline-cache layer into the entry -> now lazy()
- NewsletterOverlay imported NEWSLETTER_ISSUES from the whole NewsletterPage
module -> types + data moved to lib/newsletterIssues.ts
- SasquatchGuide (assistant overlay) lazy-loaded in Layout
- Cashu stack is now a separate chunk (useCashu), off the critical path
Net: entry down ~61%; first paint no longer parses the map, the newsletter
page, the assistant or the wallet stack.
- geolocation: cap the overall deadline at 15s (was 25s) and shorten the native
plugin attempt to 8s, so a failed fix no longer hangs ~25-40s
- MapPage: only warm the location cache when permission is ALREADY granted, so
opening /map no longer fires the browser's 'Allow location?' prompt
- MapPage: add a desktop Near Me control (it was mobile-only)
- MapPage: on native, a denial is now retryable instead of permanently opening
the browser-only help sheet
The bolt11 amount decoder is correct — the three failing tests used the
wrong invoice fixture. Per BOLT11, `lnbc2100n` = 2100 x 1e-9 BTC x 1e8
sats = 210 sats, not 21. A 21-sat invoice is `lnbc210n`. The passing
`lnbc1000n = 100 sats` case confirms the decoder's math.
Corrected the three cases in bolt11.test.ts and verifyInvoice.test.ts
to use `lnbc210n` (21 sats) so they exercise the intended 21-sat / 1-sat
slack scenarios against the correct implementation.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Replace single-assignment `let timer` with a `timers[]` array in
citrine.ts and relayProbe.ts so stricter prefer-const builds pass
(the newer ESLint pulled in by `npm audit fix` flagged the let as
never-reassigned).
- Drop two now-redundant `eslint-disable react-hooks/exhaustive-deps`
directives in NostrProvider and MapPage. Newer react-hooks no longer
reports those lines, so the directives were flagged as unused errors
under reportUnusedDisableDirectives; removing them is safe in both
old (warning only) and new (silent) ESLint.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Two type errors (surfaced by `npm run test` / tsc --noEmit on a full
toolchain; the in-browser bundler does not run tsc, so they were latent):
- useCVMClient.ts: remove a malformed cast
`spots as typeof spots & { tags; created_at }[]` feeding
deduplicateByIdentity(). The intersection dropped pubkey/kind and broke
the reassignment to `spots`. deduplicateAddressable() already returns
the correct shape, so the cast was both wrong and unnecessary.
- queryPersister.ts: import PersistedClient / Persister from
@tanstack/query-persist-client-core (their canonical source) instead of
@tanstack/react-query-persist-client, whose `export *` re-export tsc
won't resolve for named type imports. Added the core package (5.102.8,
matching the pinned wrapper) as an explicit dependency.
Both are pre-existing issues on the branch, unrelated to the audit
remediation commits.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Cover the three highest-value pure functions in the security/payments
path — previously untested:
- bolt11.getSatoshisFromBolt11: m/u/n/p multiplier decoding, amountless
invoices, 21-sat zap case, case-insensitivity, testnet/regtest
prefixes, and malformed-input throwing.
- verifyInvoice.assertInvoiceAmount: accepts matching amounts + 1-sat
rounding slack; rejects the core attack (over-charging invoice),
amountless-when-expected, malformed, and empty input.
- sanitizeUrl + companions: https passthrough, http→https upgrade,
javascript:/data:/file: rejection, relative-path rejection, plus
sanitizeCssString / urlDomain / sanitizeTel / sanitizeEmail.
Note: run `npm run test` on a full toolchain to execute these — the
in-browser builder bundles but does not run vitest.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Closes two items from the platform/completeness audit appendices.
Amber signature verification (audit "open verification item", LOW):
reconstructSignedEvent() previously trusted whatever `sig` the external
Amber signer app returned — it recomputed the event id but never
verified the Schnorr signature. A compromised or buggy signer could
therefore return a forged/garbage signature that the app would treat as
valid and publish. Now verifyEvent() is run on the reconstructed event
before it is returned; failures return null, which every caller already
handles as an "incomplete signature" error. This single choke point
covers all Amber paths: the native AmberIntent signer, AmberCallbackPage,
and AmberCallbackHandler.
F9 (LOW): pin MCP server versions in .mcp.json. Both js-dev-mcp and
nostrbook/mcp were fetched at @latest, silently pulling the newest
release into the agent tool environment every session. Pinned to the
current exact versions (0.6.3 / 0.5.3) so upgrades are deliberate.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Empty states (P4 follow-up):
- CrewsPage and EventsPage now render the shared <EmptyState> instead of
their own local copies, keeping spacing/actions/footer consistent.
Become-role routes (P1):
- Add src/lib/becomeRoles.tsx: single registry mapping each role slug to
either a CraftProfileForm config (artist/graffiti/musician/judge/tattoo/
trainer) or a lazy bespoke page (coach/filmer/editor/photographer/
builder/skater). The 6 craft roles were already thin config wrappers, so
their configs move into the registry and the wrapper files are deleted.
- Replace 12 hardcoded /become-* routes + 12 imports with one
/become/:role route (BecomeRolePage) that renders from the registry.
- Legacy /become-<slug> URLs now redirect to /become/<slug> so shared/
bookmarked links keep working.
- Update all internal links (LabPage, Index, SkatersPage, SpotDetailPage,
NIP19Page, SkaterDirectoryCard) to the canonical /become/<slug> paths.
Note: the 5 large bespoke forms were intentionally NOT merged into one
config — they carry distinct machinery (SpotPicker, wallet/Pro, geohashing)
and forcing them together would increase complexity, not reduce it.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Skaters need to know if a spot is roofed/indoor so weather info isn't
misleading — a covered or indoor spot is still skateable in the rain. Added a
4-way shelter level: outdoor (default) / partial / covered / indoor.
Data model (types.ts + skateTags.ts):
- SkateSpot.shelter parsed from a `['shelter', value]` tag (validated to known
values; absent/unknown → treated as outdoor).
- shelterOptions / DEFAULT_SHELTER / SHELTERED_VALUES + ShelterType exported.
- SHELTER_INFO display map (emoji/label/short/color) for badges.
Publishing (CreateSpotPage + EditSpotPage):
- New "Shelter" selector reusing the exact selectable-Card pattern used by
Access Type / Spot Type (cn() borders, neon ring, corner check badge).
- Emits `['shelter', value]` always, plus a queryable `['t', value]` for
sheltered spots (indoor/covered/partial) so "dry spots" filtering + Sasquatch
search work at the relay level. Outdoor adds no t tag.
- EditSpotPage initialises from the existing spot and preserves the value.
Display:
- Shelter badge on SpotDetailPage, SpotCard, and the MapPage popup — only shown
for sheltered spots (outdoor stays unbadged to avoid clutter).
Docs:
- NIP.md: shelter added to the optional-tags table, a dedicated Shelter section
explaining values + the queryable-t-tag behaviour, and an indoor example.
Build clean, no console errors.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Three low-risk speedups focused on the slowest path — questions that hit live
relay data:
1. Relay timeout 10s → 4.5s (6s for the fan-out community stats). The useful
relays answer in ~2s; the old 10s just let one slow/dead relay stall the
whole reply.
2. Results-first: for finder tools (spots/events/coaches/gear/crews/shops/
tours/stats/weather) format the CVM result locally and SKIP the second
"prettify" AI call. Removes a full model round-trip (~2–4s) — users want the
list, not extra prose. New toolCallToIntent() maps a tool call to the existing
formatLocalResults() path. Personal/Pro tools (get_my_*) still use AI synthesis.
3. Faster AI fallback: cap OpenRouter attempts to the top 3 free models before
the keyless backup, and shorten 429 backoff (≤800ms vs ≤2500ms). Avoids the
worst-case 15–20s grind through 8 models on a busy free tier.
Conversational/knowledge answers (single AI call) keep Sasquatch's personality
and are untouched.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Countdown badges (upcoming tours):
- New getCountdown() in TourCard: "In 3 days" / "Tomorrow" / "In 2 weeks" /
"Happening now" (pulsing) based on start/end dates, category-tinted
- Shown on both compact and full TourCard, and in the TourDetailPage schedule card
NIP-05 provider fix (removes obsolete nip05.social):
- New single source of truth src/lib/nip05Providers.ts with working, curated
providers: nostrcheck.me + nostr.directory (free), Nostr Plebs + nostr.band (paid)
- New reusable Nip05Required component (full-page "gate" + inline "banner"
variants) listing those providers and deep-linking to /settings?section=nip05
- Wired into CreateTourPage (gate), CreateListingPage (banner) and all four
Become* pages (Coach/Filmer/Photographer/Builder) for a consistent prompt
- SettingsPage NIP-05 screen now reads from the shared provider list
- Updated internal planning docs to match
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add a unified Schedule section across all four tour categories:
- Local: choose a fixed session (date+time) or flexible/by-appointment
- National/International: required departure + optional return trip dates
- Destruction: existing competition window, now with end>=start validation
- Reuse start-date/end-date tags for every category (parser already supported them)
- Fix datetime handling: dedicated toDateInput/inputToSecs helpers so date and
datetime-local inputs round-trip correctly (previous mix of toLocalInput +
type="date" was inconsistent)
- Consistent, logical mandatory-field validation with clear required markers
- Redesign the header into an immersive, category-tinted, grunge-striped banner
and rebuild the category selector as elegant glowing cards
- New calendar utilities (.ics + Google Calendar) and AddToCalendar component
- TourDetailPage is now fully category-aware (badge, guide label, duration,
SEO title) with a Schedule card + Add to Calendar for attendees
- TourCard shows the schedule for all categories (time for local sessions)
- Update NIP.md to document start-date/end-date across categories
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Three UX wins so the app feels "already loaded" the moment it opens:
1. IndexedDB React Query persistence (lib/queryPersister.ts)
- PersistQueryClientProvider rehydrates the last-seen spots, shops,
events, tours, crews, coaches, listings and author profiles from
IndexedDB on cold start, so returning users land on a populated map
instead of a blank one; fresh data refetches quietly in the
background.
- Strict allow-list / deny-list: only public, JSON-serializable,
cold-start-relevant queries are persisted. Private/encrypted data
(stash, DMs, contacts, wallet/nwc/cashu/bitcoin, signer) is NEVER
written to disk. 24h maxAge + version buster keep it fresh & safe.
- Pinned @tanstack/react-query + query-core + persist-client to a
single 5.102.8 to avoid a query-core mismatch (partialMatchKey).
2. Optimistic geolocation (lib/lastLocation.ts)
- getCurrentPosition now caches every successful fix.
- The map opens ALREADY centered on the user's last-known area
(city zoom) instead of the world view; "you are here" + proximity
sort seed from the cache instantly.
- "Near Me" flies to the cached location immediately on tap, then
refines to the fresh GPS fix. Silent background warm-up on web map
mount so a later tap resolves instantly.
3. Cold-start skeleton (components/MapSkeletonPins.tsx)
- When the map is genuinely empty + loading, show elegant pulsing
ghost pins + a "Finding spots" pill instead of a blank map; they
vanish the instant real (or cached) spots render. Splash now
signals content-ready when spots actually resolve.
Respects prefers-reduced-motion throughout. Build + runtime verified
clean (no console errors).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Security & reliability audit follow-up. The app was already well-hardened
(strict CSP synced across index.html/_headers/netlify.toml, careful
textContent/escapeHtml in the Leaflet DOM, broad sanitizeUrl usage,
consistent author-filtering on trust-sensitive queries). This closes the
remaining gaps:
- sanitizeUrl is now HTTPS-only: bare http:// links are upgraded to
https:// (not silently dropped); every other scheme (javascript:,
data:, file:, relative) is rejected. Removes a downgrade/mixed-content/
IP-leak vector and matches the native HTTPS-only WebView. Added a
sanitizeCssString() allowlist helper for future CSS interpolation.
- Route-level ErrorBoundary (RouteErrorBoundary) wraps the routed pages,
keyed by pathname: a crash in one page now shows a recoverable
"this page slammed → go back" message while the header/nav/rest of the
app keep working, instead of the single top-level boundary blanking the
ENTIRE app. Resets automatically on navigation.
- ErrorBoundary fallback no longer leaks stack traces to users (full
detail still logged to console for us); redesigned on-brand ("Slammed",
neon, reassures that account + data are safe). Updated its test.
Author-filtering spot-check across admin/newsletter/featured/crew/stash
paths found no gaps — all trust-sensitive queries correctly constrain
authors.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Full self-audit of the Citrine feature. Fixes found:
Robustness
- Probe now tries BOTH ws://127.0.0.1 and ws://localhost and routes to whichever
actually answers (probeCitrineEndpoints + CitrineProbeResult.url). We no longer
bet the whole feature on one loopback address form; useCitrine.url reflects the
winning endpoint so reads/writes go to the right place.
Correctness
- Preview toggle was mutating the REAL setting while in dev-preview; handleToggle
is now a no-op under preview (forced state owns the visuals).
Accessibility / "flawless" bar
- Respect prefers-reduced-motion: status pips, chip pop-in and the expand slide
now motion-reduce:animate-none (no looping glow/pulse for reduced-motion users).
- Recheck button gets an aria-label; status text is role="status" aria-live so
screen readers announce searching→detected transitions; decorative pips are
aria-hidden.
UX copy
- "Waiting" state no longer presumes Citrine isn't installed. Clearer two-step
guidance (Open Citrine → Start; come back → Recheck) covering the common
"installed but not started/detected yet" case.
Honest note: MIXED_CONTENT_ALWAYS_ALLOW (1.3.5) is the high-probability fix for
the 0-connections block but is not device-verified from here; the dual-endpoint
probe hedges the second most likely cause (address form).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Citrine is running (listening on ws://127.0.0.1:4869) but the app can't connect
and shows 0 connections — a WebView-layer block, not our logic. To diagnose
without adb/USB, the probe now returns { ok, reason } capturing which stage
failed (blocked / connect-error / timeout-no-open / opened-but-no-nostr-reply /
closed-before-open with code), and the Settings card shows a small debug readout
(probe url + reason) whenever not connected. Purely additive; no behaviour
change to routing.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Self-audit of the 1.3.0–1.3.2 Citrine feature found two real issues:
1. False-positive detection (the important one). probeCitrine resolved true on
bare socket OPEN, so ANY process squatting on 127.0.0.1:4869 would be treated
as Citrine — and we'd then route reads AND published events at a non-Nostr
peer. The probe now completes a minimal Nostr handshake: open → send a bounded
REQ with a random sub id → require a protocol-shaped reply (EVENT/EOSE/CLOSED
echoing the sub id, or a NOTICE) before declaring the relay live. Non-JSON /
non-Nostr responses fall through to a clean timeout=false. A real relay
answers with EOSE in ~ms on localhost, so the happy path is unchanged.
2. Probe churn. The reachability query re-opened a fresh ws:// probe every 5 min
and on every window focus for the whole session — needless battery/wakelock,
and a localhost poke on every Amber round-trip. Now it only polls (30s) and
refetches-on-focus WHILE still waiting for Citrine to appear; once connected
it stops the background churn (a per-session local relay won't vanish
silently, and a failed read re-probes naturally).
Reviewed and left as-is (deliberate, safe): writing every kind to Citrine incl.
gift-wrapped DMs / Stash is intended on-device encrypted backup — all sensitive
kinds are NIP-44 encrypted before reaching the publish layer, so nothing
plaintext-sensitive is mirrored. Preview flag remains render-only.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The CitrineCard is Android-only (renders null off-native), so its four visual
states couldn't be reviewed without an APK build. Adds a strictly cosmetic
preview switch:
localStorage.setItem('citrine:preview', '1') // show card, real behaviour
localStorage.setItem('citrine:preview', 'live') // force connected state
localStorage.setItem('citrine:preview', 'waiting') // force not-detected
localStorage.setItem('citrine:preview', 'searching') // force searching
localStorage.removeItem('citrine:preview') // back to normal
Safety: the flag unlocks RENDERING ONLY via isCitrinePossible(). It never sets
citrine.active and never yields a relay url, so no query or publish is ever
routed anywhere different — production relay behaviour is identical with or
without the flag. A forced state is purely visual (enabled/probing are derived
locally in the card for preview; the real probe still governs active/url).
lib/citrine.ts: citrinePreview()/isCitrinePreview() + CitrinePreviewState.
useCitrine: exposes `preview`. CitrineCard: forced state overrides the computed
state for review only.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Integrates Citrine (github.com/greenart7c3/Citrine), an Android app that runs a
full Nostr relay locally on the phone. Because it's "just a relay URL", it folds
into the existing useSkateRelays / nostr.relay() machinery with no new protocol.
What it buys SkateSpots:
- Instant reads: a localhost hit (~1ms) is queried first, remote relays run in
parallel and reconcile — every skate-content surface feels faster.
- Offline-in-the-field: reads work with no signal; writes land on-device and
publish to remote when connectivity returns.
- Resilience/backup: a durable local copy of the user's own events survives
public-relay outages and addressable-event eviction.
- Privacy: local-first data can lean on the device instead of public relays.
Design / safety:
- Android-only (Capacitor native + platform === 'android'); renders nothing and
never probes localhost on iOS/desktop web. Mixed-content ws:// is already
permitted via capacitor allowMixedContent.
- Opt-in only via config.useCitrineRelay (default off). No data is ever routed
through Citrine unless the user enables it AND a relay is actually reachable.
- Reachability probe (short-lived WebSocket) cached through react-query, shared
app-wide, re-checked periodically + on focus, with a manual Recheck button.
- When active, the local URL is prepended to reads and included in writes; the
read-relay set feeds getQueryKeySuffix so caches refetch automatically when
Citrine comes/goes.
- Encryption unchanged: Stash/DMs stay NIP-44 encrypted regardless of
destination — the Settings card states this explicitly. Local ≠ plaintext.
New: src/lib/citrine.ts, src/hooks/useCitrine.ts, CitrineCard in the Relays
settings screen. AppConfig gains useCitrineRelay (zod-validated, optional).
On-device verification still needed: confirm Android permits ws://127.0.0.1
cleartext to localhost (network security config) once Citrine is running.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
- 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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
- 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>
- 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>
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>
- Flesh out SkateJudge as a full craft-based Lab profile (parseCraftBase),
with JUDGE_FORMAT / SPECIALTY / LEVEL label sets covering Game of S.K.A.T.E.,
best trick, jams, contests, am series, world tour stops, etc.
- Wire judge into useLabProfiles (union type, self-tag, parse case)
- Add "judging" ServiceType to LabPage: tab, accent, filter sections, empty
state, counts, profile card icon, and ?craft=judging deep link
- New BecomeJudgePage + /become-judge route + FAB entry
- CoachDetailPage renders judge profiles (accent, icon, labels, noun, title)
- Community Stats "Judges" card now links to /lab?craft=judging
- Document kind 35012 (skatejudge) in NIP.md
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
1. Map filter: renamed the "Bitcoin" toggle to "Spedn" (matches the toolbar
button); tooltip now says "Spedn your sats".
2. Community stats (/stats): useGrowthStats now queries the same relay set as the
rest of the app (community relay + user relays) via useSkateRelays instead of
only the single hardcoded SKATE_RELAY. Lab profiles / spots / etc. that live on
a user's own relay are now counted, so the numbers match what /lab shows.
3. Near Me on GrapheneOS: even after granting the OS location permission, the
WebView's navigator.geolocation stays blocked → "permission denied". Fixed by
fetching the position from the native Capacitor Geolocation plugin first
(LocationManager, honours the granted permission), falling back to
navigator.geolocation only if that fails. Also auto-inject
ACCESS_FINE/COARSE_LOCATION into AndroidManifest via the manifest script.
Bump version to 1.2.0 (versionCode 10200).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause found in the plugin's native getPublicKey (NostrSignerPlugin.java):
when a packageName is set, it FIRST tries Amber's ContentProvider (background,
no-UI). On GrapheneOS that cross-app ContentResolver query is blocked and never
returns, so it never falls through to startActivityForResult(nostrsigner:) —
the branch that actually opens Amber's approval UI. Result: eternal
"Waiting for Amber".
Fix: getAmberPublicKeyViaPlugin no longer calls setPackageName and no longer
passes packageName; it passes only the get_public_key permission. With no
package name the plugin skips the content-resolver branch and launches the
intent directly, opening Amber's UI. Amber returns both pubkey and its package,
so later signing still works. Paired with the auto-injected <queries> manifest
block so the OS can resolve the intent in the first place.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Two concrete root causes for the eternal "Waiting for Amber…":
1. setPackageName could hang forever on GrapheneOS. getPlugin() awaited it with
no timeout, so getPublicKey() (and its 90s timeout) was never even reached.
Now setPackageName races a 3s timeout and continues regardless — the Amber
package is passed explicitly on every call anyway, so it's non-essential.
2. AndroidManifest.xml had no <queries> block. On Android 11+/GrapheneOS the OS
silently blocks the nostrsigner intent without it, so Amber never opens. Added
scripts/inject-amber-queries.mjs (idempotent) and wired it into android:init /
android:sync / android:build so the block is guaranteed present in every APK,
surviving cap sync regeneration.
Bump version to 1.1.9.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause: LoginDialog and useCurrentUser detected the native app via
window.Capacitor, which is unreliable inside the WebView (false at mount / not
attached in some builds). So the ZapStore APK fell into the web intent:// redirect
flow and never opened Amber ("Waiting for Amber…" hang).
Fix: add isNativeAmberAvailable() in capacitorAmberSigner.ts using the imported
@capacitor/core Capacitor + native plugin presence, and use it (evaluated at
click time) in LoginDialog.handleAmberLogin and in useCurrentUser's signer
selection. The APK now calls getAmberPublicKeyViaPlugin() → native intent → Amber
opens directly, no redirects.
Bump version to 1.1.7.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- CrewCard: rebuilt to fit any screen width — clip overflow, truncate name/location, single wrapping badge row, consistent avatar sizing; responsive header
- Amber web login: after opening Amber, drop user on manual paste step after 2.5s so the spinner never hangs when the callback opens in a new tab or clipboard is blocked; keep polling on the paste step so a late callback still auto-completes
- Stash: hasNip44/hasNip04 now fall back to the active login when pubkey matching fails, so logged-in nsec/amber users are no longer wrongly told "Encryption Required"
- Signup: added an explicit welcome step that explains account creation and only generates the keypair when the user taps "Create my account" (no more silent auto-creation on dialog open)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- signerCapabilities: recognize x-amber login type as NIP-44/NIP-04
capable (Amber implements nip44_encrypt/decrypt via native intents and
URL redirects). Fixes "Encryption Required" wall on Stash for users
logged in with Amber on Android.
- CrewDetailPage: restructure hero header so actions wrap below the
title on mobile instead of squeezing it in a cramped 3-column flex;
smaller avatar/title on small screens, truncating location.
- MapPage: remove Quick Snap from the mobile Add speed-dial and all its
handlers, refs, web-fallback input, and now-unused imports.
- zapstore.yaml: explicit orange icon + metadata_sources: [] so the
correct logo is always used on the Zapstore listing.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Address the two genuinely exploitable findings from the OpenCode audit:
C1 (critical) — sasquatchBrain.ts shipped a hardcoded OpenRouter API key in
the client bundle, extractable and abusable by anyone. Removed it; OpenRouter
is now opt-in via VITE_OPENROUTER_API_KEY and skipped when unset. Sasquatch
still works for everyone via the existing keyless Pollinations fallback.
(The leaked key must also be rotated/revoked on openrouter.ai.)
H1 (high) — SpotMap popup interpolated external weather-API data directly into
innerHTML. Added an escapeHtml() helper and applied it to every weather/AQI/
forecast value so a compromised weather API can't inject HTML/JS.
Triage of the rest: C3 (crew agent derived key) is a documented, deliberate
tradeoff — agent holds no funds and only speaks for itself. H2 (chart.tsx
dangerouslySetInnerHTML) is the stock shadcn chart with app-controlled config,
not user input. H3/H4 (GitHub Actions) don't apply — no .github/ in this repo.
C2 (NWC in localStorage) is standard for web Nostr clients. npm audit fix +
key rotation are operator steps done outside the app.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The "Tip the Mapper" card now links the avatar/name to the mapper's profile
(/{npub} via the NIP-19 router) and adds a "Message Mapper" button that opens
an encrypted DM (/messages/{npub}). Makes it easy to contact whoever added a
spot — e.g. to ask them to add photos to a photoless submission. The message
button is hidden when you're viewing your own spot.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Step 3 — member visibility (best-of-both, honest):
- MemberVisibility ('public' | 'crew') stored on the member's OWN kind-1985
attestation via a `visibility` tag, so a public crew can contain a private
member. Confirmed crew-mates + founder see them; outsiders see anonymised
"hidden member" slots. Framed honestly in-UI (app-level discovery privacy).
- VisibilityPicker in ConfirmMembershipCard (choose on join) +
MemberVisibilityControl for confirmed members to change it anytime.
- useCollectiveMembers parses visibility; CollectiveMembers hides crew-only
members from non-members.
Step 4 — anti-fraud vouch layer:
- useVouches/useVouchFor: NIP-32 vouch labels, trust-WEIGHTED via web-of-trust
so fake-account vouch rings carry no weight. VouchCard with new/vouched/
scene-verified tiers, wired into every Lab profile detail page.
Step 4 — CrewDetail UX redesign:
- Activity consolidated into clean Feed/Events/Chat tabs (no more giant scroll)
- Sidebar reordered: Members → Support → Band → Connections → Founder
- Member visibility control surfaced; tighter hero
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- New addressable kinds 35007–35011 with types, label maps and parsers
- Shared parseCraftBase helper + parseLabProfile dispatcher in useLabProfiles
- Reusable, config-driven CraftProfileForm (polished shared shell, per-craft
skill sections + extra fields, entity/collective, CLINK, Pro upgrade)
- Five thin Become* pages wired into AppRouter and the Lab "Offer" menu
- LabPage: extended ServiceType, accents, scrollable tab strip, filter
sections, counts, empty states and ProfileCard to all 10 crafts
- CoachDetailPage renders all craft types (map-driven accents/icons/labels)
plus craft-specific fields (studio, books-open, credentials, remote,
graffiti crew, music links)
- NIP.md documents all five new craft schemas
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add "punk" + "rap/hip-hop" crew vibe presets
- Hide Crew Treasury behind SHOW_CREW_TREASURY flag (detail page + form), code kept
- Improve crew band section into a dedicated card with a "Book them for a gig"
action that deep-links to a Concert event hosted by the crew
- CreateEventPage: prefill event type + title from ?type= / ?title= params
- CLINK (a): expose CLINK tip code on user profiles (EditProfileForm field +
"Tip in Bitcoin" button on profile view, stored as custom kind 0 field)
- CLINK (c): relabel "Pay via CLINK" -> "Tip in Bitcoin" with a "what's this?"
tooltip explaining it in plain language
- Remove standalone desktop Messages icon (kept in the More menu)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause: NUser's extension signer exposes .nip44 as a getter that calls
.nostr (window.nostr) and THROWS when the extension isn't present. Any
synchronous access during render crashes the whole tree — the actual culprit
was useStash's `isEncryptionAvailable: !!user?.signer?.nip44` in its return
object, evaluated every render even when logged out.
- Add src/lib/signerCapabilities.ts: hasNip44/hasNip04 helpers that determine
capability via login type + direct window.nostr probe (no throwing getter).
- useStash: use the helper for hasNip44 + isEncryptionAvailable
- useDirectMessages: replace all render-path `enabled: !!user?.signer?.nip44`
with a pre-computed nip44Available across all 4 conversation hooks
- CrewChat, CrewRequestToJoin, ConversationPage, DirectMessagesPage,
ContactsPage: use the safe helper in render/guard paths
Async accesses (inside queryFn/callbacks) are left as-is — they reject
gracefully within react-query and never crash render.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
1. Pro purchase confirmation: on a paid pass, send the buyer an encrypted
NIP-17 receipt to their own inbox (tier, expiry, unlocked perks). Best-effort
so it never blocks the purchase. Success toast now points to the DM.
2. Frictionless Pro answers: add local static-FAQ entries for "sasquatch pro",
"pro perks/version", pricing, "go pro", "upgrade" — so those questions are
answered instantly and never dead-end when the free AI model is rate-limited
(the exact failure seen when asking "what perks has the pro version?").
3. FAQ matching now prefers the longest matching key so specific phrases win
over broad ones ("sasquatch pro" > "pro").
4. Refresh the free-model fallback list (add deepseek-v3, gemini-2.0-flash-exp,
qwen-2.5-72b) for better free-tier AI availability.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Trip Planner: `plan_trip` tool orchestrates spots+events+coaches+crews+weather
into a weather-aware, day-by-day itinerary card (TripItineraryCard). Actions:
save all spots to a NIP-51 Collection, book/zap a coach via their profile,
and deep-link out to flights/stays (assist, never fake-book, no KYC).
- Free-tier resilience: local intent parser answers "spots/events/coaches/
crews/gear/weather in <city>" and full trip plans directly via CVM even when
both AI providers are rate-limited — no more dead-end fallback.
- New shared joke book (sasquatchJokes.ts): skate humor + gentle roasts of the
KYC-your-audience Bitcoin-influencer crowd. Idle joke on the empty chat state,
ethos jokes in fallbacks. Dedupes the old inline joke arrays.
- UX polish: taller chat scroll area, trip-planner suggested questions, FAQ
entries for the planner.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Adds the Sasquatch Pro supporter pass with strong, forge-resistant
verification bound to the paying user's npub — no backend required.
Verification (useSasquatchPro):
- STRONG: NIP-57 zap receipt (kind 9735). Purchase signs a zap request
tagged pro-tier/pro-expiry; the receipt embeds it, cryptographically
binding payment → identity (unforgeable by the user).
- FALLBACK: self-signed kind-32671 receipt so paying users are never
locked out if the provider emits no zap receipt.
- Platform owners always Pro. Access = max unexpired expiry.
Purchase (usePurchasePro):
- Zap to skatespots@minibits.cash (falls back to plain invoice), pay via
NWC → WebLN → manual QR. Publishes the fallback receipt on success.
Pricing (sasquatchPro.ts): 4 tiers — 7d/2k, 1mo/5k, 3mo/12k (popular),
1yr/36k sats.
UI (SasquatchProDialog): upgrade sheet with tiers, perk list, QR
fallback, "already Pro" state, extend flow.
Gating (SasquatchGuide):
- Personal tools (get_my_*) now require login + Pro; prompt only exposes
them to Pro users; tool switch guards with a pro_required nudge.
- ⚡ Pro badge when active, ⚡ Go Pro button otherwise; local upgrade
nudge for "my/mine" questions from non-Pro users.
Docs: kind 32671 + verification model documented in NIP.md.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Fixes the "brain's catching traffic" dead-end for trick/terminology
questions and reduces reliance on a single flaky free API.
- Local glossary answerer (answerFromGlossary): trick/how-to/terminology
questions ("what is a feeble grind", "how do I do a kickflip", "grosso
grind") are now answered instantly from the 150+ entry glossary with
ZERO API calls — so they never hit a rate limit.
- Wired into SasquatchGuide as (a) a fast path before the AI and (b) a
last-resort fallback when the AI is unreachable.
- sasquatchBrain now has multi-provider fallback: OpenRouter free models
(with 429 backoff) → keyless Pollinations backup. Only throws if both
fail, letting the caller fall back to local knowledge.
- Replaced the unhelpful fallback message with useful local links +
a nudge to ask about a specific trick.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Grew the glossary from ~80 to ~150 entries across 15 categories.
- Added many more tricks per the community list: impossible, gazelle
flip, pressure/hospital/dolphin flips, bigger spin/flip, suski/willy/
losi/hurricane grinds, japan/madonna/slob/seatbelt/roastbeef/benihana/
lien/cannonball/christ grabs, 720/1080, casper/anti-casper, spacewalk,
walk the dog, g-turn, and more — with inventor credits and aka names.
- New reference categories: "Transition & Vert Terms", "Stance &
Movement", and "Hardware & Equipment" (deck, trucks, bushings,
bearings, risers, wheel bite, etc.).
- Reference categories hide difficulty badges (added `reference` flag +
`hideLevel` on TrickRow); search still spans everything.
- Updated hero copy + SEO description to reflect the broader scope.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Skate scenes and local music go hand in hand — crews can now list a band
name + a music link (Spotify/Bandcamp/YouTube/Nostr).
- SkateCrew type: new optional bandName / bandUrl fields.
- Parser reads `band` + `band-url` tags (band-url validated as http(s)).
- CrewForm: dedicated optional "Crew Band" section.
- CrewDetailPage: purple band row (linked when a valid URL is present).
- CrewsPage cards: small "Band" badge when a crew has a band.
- Kept scoped to crews only (not The Lab) since bands aren't a
skate-specific craft.
- NIP.md: documented the new `band` / `band-url` crew tags.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Glossary:
- New /glossary page: 80+ tricks across 11 categories (fundamentals,
flips, shoves/spins, grinds, slides, grabs, airs, lip, stalls,
handplants, boneless, freestyle) with alternate names, difficulty
badges, live search, and category filters.
- Added to desktop "More" dropdown and mobile burger menu.
Sasquatch AI:
- Fixed/expanded free-model fallback list with valid OpenRouter slugs
(dropped the non-existent gemma-4 slug).
- Added exponential backoff + one retry on 429 rate limits, respecting
Retry-After headers.
- SasquatchGuide now reuses the shared sasquatchBrain caller instead of
a duplicated implementation.
- Friendlier fallback message when the AI is saturated.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause: @capacitor/geolocation v8 fetches position via Google Play
Services (play-services-location / iongeolocation-android). On de-Googled
devices (GrapheneOS) those classes don't exist, so the plugin's
getCurrentPosition/watchPosition throw NoClassDefFoundError on the Android
main thread -> FATAL EXCEPTION -> app killed. No JS try/catch can intercept it.
Fix: use the native plugin ONLY for the OS location-permission prompt (pure
Android, no Play Services), then fetch the actual position via the WebView's
navigator.geolocation, which uses Android's LocationManager directly and works
without Google. Removed the now-dead raceForFirstFix() and the native
position/watch typings so the Play-Services code path is never invoked.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Crews page
- "Near me" geolocation toggle with haversine distance sort + distance chips
- Crew count + cities social-proof stat header
- Deterministic per-crew vibe-colour accents (no more wall of teal)
- Activity pulse ("🔥 active this week") + OG/Verified/Established rep badges
- Rich empty state ("Claim your local scene") instead of dead "No crews yet"
Crew synergies
- Crew treasury: transparent, non-custodial NIP-75 zap goal (kind 9041) with
live progress bar tallied from zap receipts; funds settle to founder's own
Lightning wallet — no shared wallet or new wallet settings needed
- Crew-hosted events: host a jam/comp as the crew (event a-tag → crew coord),
"Hosted by [crew]" on event pages, Crew Events list on the crew page
- Crew ↔ Lab hiring pipeline: "Open Call / looking-for" flags (filmer, editor,
coach, builder, members) surfaced on the crew page
- Request to join: encrypted NIP-17 DM to founder + admins (no founder-push
bottleneck, no public join event)
- Reverse links: "Crews that skate here" on spot pages
Contacts (0xchat-style + Armada-style privacy)
- New /contacts page with Crew-mates / Follows / Recent DMs / Saved tabs
- Per-contact quick actions: message, zap, view profile, star
- Private encrypted contacts list (NIP-51 kind 30000, NIP-44 encrypted to self)
- Added to burger menu + desktop More menu (gated to logged-in users)
Docs: NIP.md updated with crew treasury, crew-hosted events, looking-for tags,
and the private encrypted contacts list.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The persistent failure on GrapheneOS (no Play Services, GPS-only) was
caused by the acquisition strategy itself, not the OS:
- The old low-accuracy-only watch never fired without a fused/network
provider, and the 25s deadline killed it before a cold GPS lock (which
takes 30–60s on GrapheneOS) could ever deliver.
Rewrite raceForFirstFix to actually work on GPS-only devices:
- Run BOTH a high-accuracy (GPS) and low-accuracy (network) watch in
parallel — high accuracy is what engages the GPS chip and is usually
the one that delivers on GrapheneOS.
- Also fire a one-shot high-accuracy getCurrentPosition.
- Extend the deadline to 60s (real cold-GPS lock time) and classify a
timeout-flavored failure correctly.
- Ignore invalid/null-island (0,0) readings.
- Drop the redundant post-race extra attempt.
UX: show a persistent "Locating you…" toast (dismissed on success/fail)
so the up-to-60s GPS lock doesn't look frozen, plus a "Found you"
confirmation. Failure still surfaces the raw error text for diagnosis.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause of the persistent Near Me failure: the test device runs
GrapheneOS with no Google Play Services, so there's no network/fused
location provider — only raw GPS, which can't fix indoors. This was
never a code bug.
- Add 'no_provider' GeoErrorKind; detect it from native error text
(disabled / provider / settings / unavailable).
- Show accurate, actionable messaging: tell the user to enable Network
Location (Settings → Location) — the GrapheneOS-friendly fix — or use
GPS outdoors, instead of a vague timeout.
- Refine the timeout copy along the same lines.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The real reason Near Me failed indoors: getCurrentPosition with high
accuracy waits for a single satisfactory GPS reading, which needs
satellite visibility and times out indoors. Google Maps feels instant
because it takes the FIRST update from any provider (wifi/cell/fused).
Rewrite the acquisition strategy to do the same:
- raceForFirstFix() starts a low-accuracy watchPosition and resolves on
the very first update (the fast network/fused fix, ~1s indoors),
racing it against a cheap cached getCurrentPosition. First to return
wins; watch is cleared immediately after.
- 25s deadline falls back to one long high-accuracy attempt for the
cold-outdoor-GPS case.
- Apply the same watchPosition-race to the web path.
- watchPosition/clearWatch added to the plugin proxy interface, guarded
so builds without them fall back to getCurrentPosition.
- Update timeout toast (drop the "walk to a window" advice; point at the
GPS/Location quick-setting toggle instead).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Solves the "spots that change appearance or disappear" problem with
crowd-sourced, trust-weighted status reports — since the spot owner
often isn't the one who notices a spot got knobbed/demolished/busted.
New custom kind 2259 (regular, append-only): any user can report a
spot's current physical state — active / bust / skate-stopped / closed /
gone — with an optional note, referencing the spot via its `a`
coordinate.
Client aggregation (useSpotStatus / useBatchSpotStatus):
- recency-weighted (30-day half-life) so fresh reports dominate and a
rebuilt spot naturally recovers
- trust-weighted via existing useBatchTrustScores (0.2–1.0 multiplier)
- most-recent-report-per-reporter (no ballot stuffing)
- reports older than 180 days ignored
UI:
- SpotStatusBadge (colored consensus badge + tooltip w/ report count)
- ReportSpotStatusDialog (status picker, optional note, recent reports)
- Badge + Report Status button on SpotDetailPage
- Batched status badge on SpotCard; "gone" spots dimmed/grayscaled
- "gone" spots hidden from map markers (still listed dimmed) to keep the
map uncluttered while preserving the record
Documented the kind, tags, status values, and aggregation rules in
NIP.md.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Now that the native crash is fixed (Kotlin 2.1 alignment on the Android
side), the remaining issue was GPS "Location timed out" on a cold first
fix / indoors. Rework the native position acquisition into three
increasingly-forgiving stages that return the first success:
1. INSTANT — accept a cached/coarse fix up to 5 min old (resolves
immediately on a warm chip).
2. PRECISE — high-accuracy GPS with a generous 30s window so a cold
first-fix has room to complete.
3. COARSE FALLBACK — network/wifi fix with the longest window to
rescue the "GPS can't lock indoors" case.
Also bump the web-path timeout to 20s, and make the "Location timed out"
toast actionable on native (suggests stepping near a window / retrying).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The "Near Me crashes the app / bounces to ZapStore" bug is a native
FATAL EXCEPTION on the Android main thread, which no JS try/catch can
intercept. registerPlugin('Geolocation') always returns a proxy even
when the native class isn't actually registered in the APK (e.g. after
an incomplete cap sync or missing play-services-location), and invoking
a method on that ghost proxy is what detonates the crash.
Fix: verify the plugin is genuinely registered via
Capacitor.isPluginAvailable() and the live window.Capacitor.Plugins
registry BEFORE ever calling it. If it's not truly present, fall back to
the WebView's navigator.geolocation instead of crashing. Also guard the
requestPermissions step independently so a permission-flow throw can't
take down the app. isNativeGeolocation() now reflects the path actually
in use so error copy matches; added isNativePlatform() helper.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The native Geolocation fix only takes effect once the APK is rebuilt — the
current phone build still uses navigator.geolocation, hence the error. To make
the rebuilt version more robust:
- If a high-accuracy fix fails/times out (common indoors or weak GPS), retry
with a coarse (network/wifi) fix and a longer window before giving up. This
rescues most "Location unavailable" cases.
- On native, the "unavailable" toast now tells the user to turn on Location/GPS
AND allow the app permission, instead of the generic web message.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Symptom: crew logo upload spins forever, and crew creation appears to do
nothing. Both go through Amber signEvent. Login (getPublicKey) works because
it uses Amber's silent content-resolver path, but signEvent must launch
Amber's approval Activity and wait for the result — and if that result never
comes back (buggy plugin build, user backs out, missing permission) the
bridge promise never settles, so the UI hangs with no error.
Changes:
- withTimeout() wraps every Amber bridge call (sign/getPublicKey/nip04/nip44)
with a 90s cap. A hung round-trip now rejects with a clear, actionable
message instead of spinning forever, which also unblocks the CrewForm
(isUploading/isPublishing finally resolve).
- setPackageName is now called ONCE per session (cached promise) instead of
before every op — repeated calls are needless bridge chatter and can wedge
on some builds.
NOTE (root cause, fixed on the build machine): package.json requests the
GitHub `main` build of nostr-signer-capacitor-plugin, but package-lock.json
still pins the old npm 0.0.5 (which the APK bundles). 0.0.5 is the version
prone to the signEvent activity-result hang. Regenerate the lockfile with
`npm install` so the `main` build is used — see chat for steps.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>