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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
- 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>
- 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>
- 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>
- Move bubble & panel to bottom-RIGHT on desktop (was bottom-left, colliding
with / hiding behind the desktop footer). Mobile stays bottom-left.
- Raise bubble/panel z-index to z-60 so it always sits above the footer.
- Re-clamp a stale saved drag position on mount: a bubble dragged off-screen
on one device no longer disappears on another.
- Stop showing the long welcome greeting + action button on every open once a
conversation has started (chatMessages > 0), so it no longer "says the same
thing all over again".
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>
- useGrowthStats now queries and exposes every Lab craft individually: artists,
graffiti, tattoo, trainers, musicians and judges (previously folded into an
opaque "other" bucket with no visible breakdown).
- CommunityStatsPage renders a card for all 11 Lab crafts, so the section matches
what /lab actually lists instead of only showing 5 categories.
- labTotal + the content-mix donut's "Lab" slice now sum every craft, keeping the
"total" badge and chart consistent.
Note on relay mismatch: the app dedupes addressable events (keeps newest per
pubkey+kind+d) and merges across the community relay + the user's relays, so
counts can legitimately differ from a raw single-relay browser that shows stale
duplicate versions.
Bump version to 1.2.1 (versionCode 10201).
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>
Login (Amber tab):
- Emphasize the proper NIP-55 redirect flow: one tap → approve in Amber →
redirected back to /amber-callback → poll auto-completes login. No copy/paste.
- Replace the indefinite spinner with a clear "Waiting for Amber…" state that
auto-signs-in on return, with an "Open Amber again" retry.
- Demote manual pubkey entry to an explicit "Having trouble?" escape hatch that
the user must tap — it never hijacks the flow anymore.
- Removed the aggressive 2.5s auto-switch to paste; keep background polling on
both waiting and paste states so a late callback still completes.
Signup:
- Welcome step now recommends "Sign up with Amber" first on Android (key lives
inside Amber, SkateSpots never touches it), with a clearly-secondary "quick
account on this device" option. Wired onUseAmber through LoginArea to open the
Amber login flow.
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>
The guide's action buttons used window.location.href = '/onboarding', a hard
full-page navigation. In the SPA/native (Capacitor) context this reboots the
app at its entry point (home) rather than resolving the client-side route, so
tapping "Learn More" dumped users back on the home page.
Switched all Sasquatch guide navigation (Learn More, Login to begin, Login to
Offer Services / Create a Crew, Start exploring) to React Router's navigate(),
which performs proper in-app client-side routing and reliably renders the
target page.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Since signup now defers the key backup, users who skip it MUST be able to get
their recovery code later — otherwise we've just moved the trap. This adds a
"Your Recovery Code" card at the top of Settings > Security that:
- For nsec logins: reveals the recovery code on demand (hidden by default),
with Copy, Download (.txt), and QR options, plus a Hide button and clear
"never share this" warnings.
- For bunker / browser-extension / Amber logins: explains that SkateSpots
never held the key and points the user to back it up in their signer app.
Also removed the old placeholder QR handler that only showed an error, and
updated the "Backup Not Confirmed" copy to direct users to the new reveal
card instead of falsely claiming lost keys are unrecoverable.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- "Sign Up" now goes straight into a one-tap flow: a key is generated silently
and the user is logged in immediately, landing on optional profile setup.
- Removed the method-choice dialog (mnemonic vs nsec) and the generate/download
steps from the default path — no crypto vocabulary, no gates.
- Removed the forced key-download gate (Continue was disabled until download).
Backup is now optional and deferred (BackupReminder already nudges post-login;
the "done" step offers a gentle, jargon-free recovery-code reveal).
- Softened copy throughout: "secret key" -> "recovery code / account backup",
"your identity on Nostr" -> plain reassurance, CTA -> "Save & Start Skating".
- Advanced signer-app path preserved under a subtle link.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- NofferField: link → https://clinkme.dev/specs.html (instead of just /)
- EditProfileForm: link → https://clinkme.dev/specs.html (instead of just /)
- SasquatchGuide: added [specs] and [apps] inline links next to the clink description, pointing to the new pages
The app already referenced clinkme.dev homepage; these add the two new deeper pages for discoverability.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Completes the admin moderation coverage from the previous commit.
Lab profiles (coaches, filmers, editors, photographers, builders, artists,
graffiti, tattoo, trainers, musicians):
- useLabProfiles / useLabProfile and useCoaches / useCoach now filter out
admin-hidden entries (list) and 404 hidden ones (detail) via the shared
moderation layer. Added filterAdminHiddenMixed() for the mixed-kind Lab
union (kind read from each item's event).
- CoachDetailPage (the shared Lab detail page) now shows the delete/takedown
card to admins too, not just the owner, with admin-specific copy.
DeleteEntityDialog already handles the owner-or-admin logic.
Gear / marketplace listings (NIP-99, kind 30402):
- useMarketplaceItems / useMarketplaceAtSpot filter out admin-hidden listings.
- GearSwapItemCard: admins get a "Remove (admin)" action in the card menu with
a confirmation dialog that routes through useDeleteEntity (NIP-09 delete +
hidden label). Report stays for non-owners.
SECURITY.md: updated the admin-moderation coverage list and auditor file map.
Build + lint pass, no console errors.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The two SkateSpots admin pubkeys (already in src/lib/admins.ts) can now remove
any listing, not just their own. Because a NIP-09 deletion signed by a
non-author isn't honored by most relays, admin takedowns are made reliable with
a client-side moderation layer:
- useDeleteEntity: owners still publish a plain NIP-09 kind:5. Admins acting on
someone else's content publish the kind:5 AND a NIP-32 kind:1985 `hidden`
label referencing the target coordinate/id, authored by the admin.
- useAdminModeration (new): fetches admin-authored `hidden` labels app-wide and
exposes a fast lookup + filterAdminHidden() helper. Only labels whose author
is in the hardcoded ADMIN_PUBKEYS are honored (author = trust boundary,
re-verified client-side; never derived from a d-tag).
- Wired the hide filter into the list + detail hooks for spots, crews (incl.
by-focus), tours, events, and shops via React Query `select`, so removed
content disappears from lists and 404s on its detail page.
UI:
- DeleteEntityDialog now renders for owner OR admin, with distinct
"Remove (admin)" framing and copy (used on Crew, Coach, Tour, Event pages).
- SpotDetailPage & ShopDetailPage: routed their inline delete through
useDeleteEntity, show the delete button for admins, and relabel the confirm
dialog for admin takedowns.
- Added the delete control to Tour and Event detail pages (previously none).
SECURITY.md: documented the admin moderation trust boundary and extended the
auditor file map. Verified build + no console errors.
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>
An NWC connection string is a spend credential stored client-side. If a
connection is granted an unlimited budget, a compromise could drain the
wallet up to that limit. Add a prominent security notice in the "Connect
NWC Wallet" dialog instructing users to set a small daily budget and scope
the connection to pay_invoice only, and reminding them it can be revoked in
their wallet. This caps the blast radius of the credential the app must hold.
(The CSP referenced in the report was already added in the prior commit via
public/_headers; storage of nsec/NWC strings in localStorage is inherent to
client-side Nostr signing/payments and is mitigated by the XSS + CSP work.)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Critical: Leaflet marker popups built untrusted Nostr event fields (and
BTC Map API fields) into innerHTML, bypassing React's output encoding. Any
user could publish a spot/tour/crew/event with an <img onerror> payload in a
text field; it executed on /map render with no interaction, letting injected
script read the plaintext nsec from localStorage (nostr:login) and the Cashu
seed (cashu-mnemonic) — permanent, unrotatable account + wallet takeover.
Fixes in src/components/SpotMap.tsx:
- Rebuild all popup DOM with createElement + textContent instead of innerHTML
for every untrusted field: spot.location; tour.title/location/meetingPoint;
evt.title/location; crew.name/location; BTC Map name/address/opening_hours/
phone/description.
- Validate every event/API URL with sanitizeUrl() before it reaches an
href/src: crew.image (popup + marker icon) and BTC Map website. Drop
anything that isn't http(s). tel: links reduced to safe characters.
- Replace inline onerror handler on the crew marker image (would be blocked
by CSP anyway) with a scripted img.onerror fallback.
- Add popupRow() helper so untrusted detail rows can never be innerHTML'd.
Defense in depth (web only, native Capacitor build unaffected):
- Add public/_headers with a strict Content-Security-Policy: default-src
'none'; script-src 'self' 'wasm-unsafe-eval' (no unsafe-inline/eval);
object-src 'none'; base-uri 'none'; frame-ancestors 'none'; plus the
https:/wss: allowances a Nostr client genuinely needs for relays, media,
and mints. Also X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
Permissions-Policy.
- Move the inline boot-crash-guard script to public/boot-guard.js so
script-src can stay 'self' with no 'unsafe-inline'; index.html now loads it
externally. The guard also now paints errors via textContent, not innerHTML.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
useCrew previously relied on a single combined filter (kind + #d + authors,
limit 1). Some relays don't reliably match combined author + d-tag filters,
causing the crew detail page to render NotFound even when the crew exists.
Now, if the precise addressable lookup returns nothing and the author pubkey
is known, we fall back to fetching that author's crews (kind + authors) and
matching the identifier client-side. This survives relays with quirky tag
indexing while still preferring the exact match when available.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Latitude/longitude fields are now optional for local/national tours
- If provided, both must be filled and valid; otherwise geohash and
coords tags are omitted from the published event
- Updated input placeholders and added helper text clarifying coordinates
are optional
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
CreateTourPage called usePlatformFee('featuredTour') but never imported the
hook, causing a "usePlatformFee is not defined" ReferenceError that crashed
the /create-tour page. Added the import to match the other pages that use it.
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>
Add issue #5 (2026-08-08) to NEWSLETTER_ISSUES with a Groucho-Marx satiric
tone. Highlights: the 3x-bigger Lab (10 crafts), the Lab mobile grid fix,
Crews covering every scene with opt-in visibility + web-of-trust vouching,
the redesigned CrewDetail page, and Music<->Events interconnect. Mocks
"OpenCrack" for volunteering to fix Bitcoin's crypto libraries, and owns
that the app has never been audited (one day it will be — and we'll shoot
ourselves in both feet). Includes matching nostrText teaser for kind 1.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
On mobile the 10 craft filters (Coaches, Filmers, Photo, Editors, Builders,
Artists, Graffiti, Tattoo, Trainers, Music) were in a sideways-scrolling
strip, hiding options off-screen. Switch to a 5-column grid on phones (two
tidy rows of five) while keeping the flex-wrap layout on desktop, so every
craft is visible at a glance with no horizontal scrolling.
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>
- BetaDisclaimerDialog: shown once on first load, versioned storage key
(bump DISCLAIMER_VERSION to re-show after terms change), soft single
"Got it" button, matches dark + #ff0040 style, cannot be dismissed by
outside click/Esc
- Permanent BETA badge in desktop footer
- Wired into App.tsx
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The public profile page overflowed sideways on phones, pushing the Info /
Contributions / Crews sidebar off-screen. Cause: the About text contained a
64-char unbreakable URL (ContextVM server link) with no word-breaking, forcing
the layout wider than the viewport.
- About text now uses break-words + overflow-wrap:anywhere so long URLs wrap
- Added min-w-0 to both grid columns so they can shrink instead of overflowing
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The app had two things both called "Profile": the /settings editor and the
public /npub page. They're not duplicates (edit vs. view) but the naming and
lack of links between them was confusing. Now:
- Public profile (/npub) gets an owner-only "Edit Profile" button that deep-
links to /settings?section=profile
- Settings row renamed "Profile" -> "Edit Profile" (section title already said
Edit Profile); it already has a "View my public profile" link back
- Result: one Profile (the public page), one obvious way to edit it, linked
both directions — the standard IG/GitHub pattern
Bonus fix: mobile menu "My Data" used a broken ?tab=mydata param; corrected to
?section=mydata (the settings page reads ?section=).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Profile page (NIP19Page):
- Add "member of crew X" summary under the header (with founder crown)
- Add a full Crews sidebar card listing every crew the user founds/belongs to
- Add crews count to the Contributions stats (now Spots/Shops/Crews)
- Show a Bot badge for kind 0 bot accounts
- CLINK "Tip in Bitcoin" already surfaces on profiles (from prior work)
- New useUserCrews(pubkey) hook powering the above
Messages:
- Replace paste-only "New Message" dialog with NewMessageDialog: a searchable
people picker (crew-mates + following, grouped, name-searchable) that also
accepts a pasted npub/hex as a fallback. Starting a DM now feels like a
real contacts screen.
Map toolbar consistency:
- Consolidate secondary add actions (Shop / Gear / Tour) into a single
"+ More" popover; keep primary "Add Spot" prominent. 4 buttons -> 2 controls.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
#0 Crew band gig booking:
- Rename "Book them for a gig" -> "Set up a gig" (honest: it creates an event)
- Add "Message about a gig" -> pre-filled encrypted DM (NIP-17) to the crew
founder via existing /messages/:npub?message= flow (real communication)
#1 Profile access (was missing/mislabeled everywhere):
- Desktop header: add "View my profile" icon linking to /npub
- Mobile user menu: split mislabeled "My Profile"(->Settings) into
"View Profile" (-> /npub) + "Settings"
- Burger menu: add "View Profile" entry
- Settings > Edit Profile: add "View my public profile" quick link
#2 Remove standalone Local Chat button on desktop map toolbar (kept on mobile)
Audit fixes:
- Mobile menu "Wallet & Stash" -> "Wallet" (Stash is already its own item)
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>
"Start a Crew" was ambiguous (team? chat? listing?). Renamed to the clearer
"Create Crew" across the Crews page button/empty state, Create Crew page
title/header/login gate, and Sasquatch FAQ. Added a subtitle distinguishing a
Crew (skate WITH) from The Lab (hire).
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>
The NUser extension signer's .nip44 getter calls .nostr which throws
"Browser extension not available" when window.nostr is absent. A try/catch
around ?.nip44 doesn't work because esbuild optimises it away.
Fix: check login type + window.nostr directly (no getter, no throw) to
determine nip44 availability, and wrap all actual signer accesses at
call-time in their own try/catch.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Some signers throw "Browser extension not available" when the .nip44
getter is accessed rather than returning undefined. Wrap the check in a
try/catch so useStash never throws on mount for logged-out users or users
with signers that don't support NIP-44.
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>
Sasquatch can now DRAFT actions from natural language and publish them
only after the user reviews & confirms — it never publishes on its own.
- useSasquatchActions: publishes confirmed drafts via the app's normal
publish path. Supports post_status (NIP-38 kind 30315, auto-expiring)
and list_gear (NIP-99 kind 30402, skateboarding-tagged), reusing the
existing event schemas.
- SasquatchActionCard: inline "review & confirm" card with editable key
fields (status text / listing title, price, condition), Publish +
Cancel, and done/cancelled states. Nothing is signed until Publish.
- SasquatchGuide: new draft_action tool (Pro-gated). The AI emits a
draft; a card renders in-chat; confirmDraft signs & publishes. Non-Pro
users get an upgrade nudge. Draft lifecycle tracked per message.
- Prompt teaches the model to use draft_action for "post/sell/list/set
my status" requests and to fill gear fields from the stash first.
Examples now working (Pro): "post that I'm skating Southbank for 3 hours"
· "list my Spitfire wheels for 8000 sats".
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>
Sasquatch can now answer questions about the logged-in user's OWN data
("what's in my stash?", "my crews", "events near my crew", "my spots",
"what am I selling?").
- New usePersonalContext hook: client-side getters for the user's Stash
(decrypted locally), crews (founder/member), marketplace listings, and
created spots. Reads own data only — nothing personal leaves the device.
- Registered 4 personal tools (get_my_stash / get_my_crews /
get_my_listings / get_my_spots) in the Sasquatch tool switch.
- Personal-tools prompt block is appended to the system prompt ONLY when
a user is logged in, so logged-out UX is unchanged.
- needsLiveData now routes "my/mine" questions to the AI tool path.
Phase 1 is intentionally ungated so we can validate the experience
before wrapping the Pro paywall (Phase 3).
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>
Probes the ContextVM relay for the CVM server's own events and reports
online/stale/offline with a last-seen timestamp. Gated to the two
platform owner pubkeys (renders null for everyone else). Notes that
Sasquatch's data layer queries relays directly and is unaffected by the
server being down.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Option B: a lightweight reorder / show-hide list for the desktop header.
- useDesktopNavPreferences: separate localStorage-backed preference for which
catalog items appear as primary top-nav tabs and their order (Map fixed
first, no hard slot cap). Reuses the shared NAV_CATALOG.
- DesktopNavManager: settings UI to reorder primary tabs (up/down) and toggle
items between the top bar and the "More" menu; Map locked; reset to default.
- Layout desktop header now renders fixed Map + user's primary tabs, and the
"More" dropdown dynamically lists the hidden catalog items (auth-gated),
so each item appears in exactly one place.
- Manager shown only on md+ screens (mobile uses the bottom-bar customizer).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The "1st tap does nothing, 2nd/3rd works" bug came from relying solely on
pointer-capture tap detection, which is flaky on the first interaction after
mount in mobile WebViews — and the earlier e.preventDefault() on pointerup was
suppressing the click we could otherwise use.
Fix: add a plain onClick as a reliable open path (the most dependable "tapped"
signal in a WebView), extract an idempotent openPanel() used by both pointerup
and click, and drop the preventDefault so the click isn't swallowed. The
justOpenedRef lock still blocks the ghost-click that would re-close it. Double
firing is harmless since openPanel just sets visible=true.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
On touch devices, the bubble's pointerup opened the panel and unmounted the
bubble; the synthesized "ghost click" from the same tap then landed on the
panel's minimize/close button (which renders in roughly the same bottom-left
spot), instantly re-closing it.
Fix: preventDefault/stopPropagation on pointerup to suppress the ghost click,
guard releasePointerCapture against unmount throws, and add a 400ms
"just opened" lock so stray close/minimize events from the same tap are
ignored.
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>
Previously useAuthor only hit the global profile-aggregator relays as a
fallback when the main pool returned zero kind 0 events. On the curated skate
relay many followed users have no profile, so names fell back to genUserName.
Now query the main pool and aggregators (purplepag.es, nostr.band, nos.lol,
damus) in parallel and keep the newest kind 0, so real display names resolve.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- reqRouter now always queries the community relay (SKATE_RELAY) first,
even when a user hasn't added it to their read list, so curated skate
content takes priority in NPool's first-seen dedup ordering
- eventRouter publishes to the catching relay first, then user write relays
- Gated behind the existing useSkateRelay setting (default: true)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add useNavPreferences hook (localStorage) with a curated catalog of pinnable
destinations (Lab, Events, Crews, Wallet, Stash, Collections, Contacts,
Messages, Stats), defaults, normalization/validation and login-gating
- Map (Shops & Spots) is fixed on the left; Profile/Login fixed on the right;
the 3 middle slots are user-customizable — nothing is ever lost since the
burger menu still lists everything
- CustomizeNavSheet: bottom-sheet editor with a live preview bar (locked slots
shown with 🔒), a toggle grid with a 3/3 counter + "bar full" state, up/down
reordering, and reset-to-default. Draft state so changes are cancelable
- Entry points: long-press (or right-click) any bottom tab, plus a "Customize
nav bar" item in the burger menu
- Login-gated tabs are hidden from logged-out users and gracefully fall back if
a saved layout references them
- Resolve tabs in Layout and pass to MobileNav as props (avoids any module-init
ordering issues with the hoisted MobileNav declaration); defensive ?? [] guard
Docs unchanged (client-only preference, no new Nostr kinds).
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>
Expanded Drop #003 from 7 to 16 update cards covering everything actually
shipped this session (per git log), grouped: the geolocation war (crash
fix, parallel first-fix GPS, progress UI, you-are-here pin, crash-proofing,
no-provider messaging), Spot Status Reports (reporting, trust/recency
consensus, badges, gone-spot hiding), Quick Snap camera-first + snap→Add
Spot, Amber signing timeout + plugin fix, crew/Lab DELETION via NIP-09
(framed as the new deletion capability, not the existing features), and
native build plumbing.
Cranked the tone harder throughout and leaned fully into the "weekly
newsletter, shipped whenever the f*** we want" bit in both the vibe line
and masthead. Updated nostrText to match.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
New issue covering everything shipped this session, written funny:
- Near Me crash fix (Kotlin 2.1 alignment)
- Parallel first-fix GPS strategy that works on GrapheneOS / no Play Services
- "Locating you…" / "Found you" progress feedback
- "You are here" user-location pin
- Community Spot Status Reports (active/bust/skate-stopped/closed/gone)
- Gone spots hidden from map markers
- Defensive native geolocation hardening
Also updated the masthead tagline to lean into the "weekly newsletter,
shipped whenever the f*** we want" bit. Includes ready-to-post nostrText
for Drop #003.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
After Near Me centers the map, there was no indicator of where the user
actually is. Add a Google-Maps-style location marker, brand-flavored:
- Circular badge using the SkateSpots logo (public/images/user-location.png)
with a red (#ff0040) ring, sitting at the user's exact coordinates.
- Pulsing translucent accuracy halo (ssPulse keyframes) so it reads as a
live "you are here" and doubles as subtle motion.
- Rendered above all other markers (zIndexOffset), with a "You are here"
popup. Created/moved/removed reactively from a new `userLocation` prop.
Wired MapPage's userLocation state through to SpotMap (lat/lon → lat/lng).
Marker image path uses import.meta.env.BASE_URL so it resolves on both
web ('/') and the Capacitor APK ('./').
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>
Append the underlying native/browser error message to the failure toast
(and log it) with a 12s duration, so the exact failure reason is visible
on-device without needing adb. This lets us distinguish OS Location-off,
provider-unavailable, timeout, and plugin errors at a glance.
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>