WebSocket connections take longer to establish on first load in
Capacitor. Increase timeout from 8s to 15s and retries from 3 to 5.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The Content Resolver silent path returns a cached/wrong key.
Passing null skips it and always opens Amber UI so the user
explicitly selects their key and the correct pubkey is returned.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
NostrSigner.npubToHex() is called before putting result in "npub" field,
so the value is already a 64-char hex pubkey. Skip nip19.decode() and
use it directly. This was causing the "data must be at least 8 chars" error.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The actual plugin source uses positional args, not object params:
- getPublicKey(packageName)
- signEvent(packageName, eventJson, id, npub)
- nip04Encrypt/Decrypt(packageName, text, id, pubKey, npub)
- nip44Encrypt/Decrypt(packageName, text, id, pubKey, npub)
- isExternalSignerInstalled(packageName)
Previous implementation was based on wrong README docs.
Now based on actual dist/esm/index.js source.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Use named export { NostrSignerPlugin } not mod.default
- setPackageName takes a plain string not { packageName: string }
- isExternalSignerInstalled takes packageName as argument
- These were the exact bugs causing "plugin not available" error
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Android 11+ package visibility rules cause isExternalSignerInstalled() to
return false even when Amber is installed, unless <queries> is declared in
AndroidManifest.xml. Skip the pre-check and attempt login directly instead,
with better error messages for cancel vs not-found vs other errors.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- vite.config.ts: mark nostr-signer-capacitor-plugin as external so
Rollup doesn't try to bundle it (it's resolved at runtime by Capacitor)
- en.ts / es.ts: remove duplicate lab key that was causing esbuild warnings
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add CapacitorAmberSigner: a full NostrSigner implementation backed by
the native nostr-signer-capacitor-plugin (login, signEvent, nip04,
nip44 all go through Amber via native Android intents)
- LoginDialog: on Capacitor, call getAmberPublicKeyViaPlugin() directly
instead of the intent:// URL redirect — returns a Promise, no
callback URL or polling needed. Shows clear error if Amber not installed.
- useCurrentUser: on Capacitor amber logins use CapacitorAmberSigner
so every publish/sign goes through the plugin, not window.location.href
- Web/PWA path unchanged — still uses URL redirect + AmberSigner
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- LoginDialog now uses openAmberForLogin() which sends the proper
intent:// URL with a callbackUrl instead of the old nostrsigner:
scheme that only copies to clipboard (broken on most setups)
- Add polling loop (500ms, 3min timeout) for amber_pending_pubkey
written by AmberCallbackPage — works even when Amber opens the
callback in a new browser tab (GrapheneOS/Android behaviour)
- visibilitychange listener now waits for app to regain focus before
attempting clipboard read, fixing race condition
- DeepLinkHandler pre-captures ?event= param into sessionStorage
when Amber fires appUrlOpen deep link in Capacitor, so
AmberCallbackPage can always find the result
- Add 'paste' to amberStep union type (was missing, caused TS error)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The chebizarro npm version has a different API to the Coracle fork that
Welshman/Coracle actually use in production. Now using:
github:coracle-social/nostr-signer-capacitor-plugin#main
Correct API (from Welshman nip55.js source):
NostrSignerPlugin.setPackageName(packageName: string) // string not object
NostrSignerPlugin.getPublicKey(packageName?: string) // packageName as arg
NostrSignerPlugin.isExternalSignerInstalled(packageName?)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
All previous URL-scheme approaches (nostrsigner:, intent:) were fundamentally
wrong for a Capacitor app. JavaScript in a WebView cannot reliably dispatch
Android intents via URL navigation.
The correct solution — used by Coracle and other Capacitor-based Nostr apps —
is the nostr-signer-capacitor-plugin which communicates with Amber via native
Android intents through the Capacitor bridge:
NostrSignerPlugin.setPackageName({ packageName: 'com.greenart7c3.nostrsigner' })
NostrSignerPlugin.isExternalSignerInstalled()
NostrSignerPlugin.getPublicKey() → { npub }
This is synchronous from the JS side, handles the entire Android intent
roundtrip natively, and returns the pubkey directly. No URL encoding,
no callback URLs, no redirects, no malformed request errors.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
GrapheneOS Vanadium browser blocks intent: URLs from web pages as a
security hardening measure. This is why every URL format we tried
caused "malformed request" — Vanadium was blocking the intent before
Amber ever saw it.
New flow that works on ALL Android browsers including GrapheneOS:
1. Tap "Open Amber" → navigates to nostrsigner: URL with no callbackUrl
(Amber copies pubkey to clipboard when no callbackUrl is given)
2. User approves in Amber, comes back to app
3. App tries navigator.clipboard.readText() automatically on visibilitychange
4. If clipboard permission denied → shows paste input for hex pubkey
5. User pastes pubkey → logged in
No intent: scheme, no callback URL, no redirect loop. Simple and bulletproof.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Three compounding bugs fixed:
1. WRONG FORMAT: Was using nostrsigner: directly, then intent: with encoded
values. Amber README shows intent: with raw unencoded S.key=value params.
2. # IN CALLBACKURL: Our callbackUrl was https://.../#/amber-callback which
contains a # — but intent: uses # as its own delimiter (#Intent;...;end).
Having a # inside the callbackUrl value breaks the entire intent string.
Fixed: callbackUrl is now https://.../amber-callback?event= (no hash).
3. HASHROUTER MISMATCH: Amber opens /amber-callback (no hash) but HashRouter
only handles /#/route paths. Fixed in main.tsx: pre-React IIFE detects
/amber-callback path, saves ?event= to sessionStorage, then redirects to
/#/amber-callback so React Router matches AmberCallbackPage correctly.
Final correct intent URL format:
intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;S.callbackUrl=https://skate-spots.org/amber-callback?event=;end
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause of "malformed request":
- buildIntentUrl() was calling encodeURIComponent() on ALL values including
callbackUrl, which was ALREADY a full URL with special chars
- This double-encoded the callbackUrl, arriving at Amber as garbage
- The intent: wrapper format was also unnecessary
Fix: back to clean nostrsigner: format per NIP-55 spec
- Only callbackUrl gets encodeURIComponent (once)
- All other params are plain query string values
- Matches exactly what NIP-55 spec shows in its example code
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The nostrsigner: scheme used directly caused "malformed request" in Amber
when opened from a browser context. The correct format per Amber's README
for web clients is the Android intent: URL format:
intent:<payload>#Intent;scheme=nostrsigner;S.type=...;S.callbackUrl=...;end
Updated all Amber operations (login, sign, nip04/nip44 encrypt/decrypt)
to use buildIntentUrl() helper instead of raw nostrsigner: URLs.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
window.open(..., '_blank') opens a new browser tab instead of firing
the Android intent — causing Amber's "malformed request" error.
Per NIP-55 spec the correct method is window.location.href = url,
which triggers the nostrsigner: intent that Amber is registered to handle.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
BUG #1 (critical): buildCallbackUrl() was returning
https://skate-spots.org/amber-callback?event=
but the app uses HashRouter, so React Router never matched /amber-callback.
Amber would open the callback URL in a new tab, land on the homepage, and
AmberCallbackPage would never render — login was silently broken.
Fixed to:
https://skate-spots.org/#/amber-callback?event=
BUG #2: AmberCallbackHandler (global, in App.tsx) was competing with
AmberCallbackPage on the /amber-callback route, causing a race condition
where the global handler could consume the pubkey before the page component.
Fixed by detecting the hash path and early-returning when on /amber-callback.
BONUS: openAmberForLogin() now persists amber_return_path from the current
hash path so AmberCallbackPage can navigate back to where the user was.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The entire nostrconnect relay approach was wrong for a native Android/Capacitor app.
Amber on Android uses NIP-55 intents (nostrsigner: URI scheme), not NIP-46 relays.
New flow (same as Amethyst):
1. openAmberForLogin() fires nostrsigner:?type=get_public_key&callbackUrl=...
2. Amber opens, user taps approve
3. Amber calls back to org.skatespots.app://amber-callback?event=<pubkey>
4. AmberCallbackPage writes pubkey to localStorage('amber_pending_pubkey')
5. LoginDialog polls localStorage every 500ms and calls login.amber(pubkey)
6. Done — no relays, no waiting, no spinning
Also added login.amber(pubkey) method to useLoginActions.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The nostrconnect flow was incorrectly creating a bunker login and trying
to do NIP-46 key exchange. Amber signs as the user's own key directly,
so the pubkey from the connect response IS the user pubkey. Now stores
a proper amber login which uses AmberSigner via useCurrentUser.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Replace one-shot visibility handler with pollRelay() called immediately on foreground + every 3s
- Add elapsed time counter (M:SS) so user can see the app is still trying
- Add 'Check Now' button for manual relay poll without generating a new URI
- Poll interval auto-clears on success, timeout, or cancel
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Replace wss://relay.primal.net with wss://nos.lol (more reliable for NIP-46)
- Shorten timeout from 10 minutes to 20 seconds
- Fix stale closure bug: track amberStep in amberStepRef so timeout can correctly reset UI
- Add 'error' step with red UI and 'Try Again' button instead of endless spinner
- Extend since filter from 5s to 10s in the past
- Add console logs for URI, relays, incoming events, decryption result, secret matching
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Index.tsx: remove Nostr.com button from homepage footer (next to FAQ/About)
- SupportersPage: change 'View on GitHub' → 'View on NostrHub' with correct naddr URL
- Layout: remove 'Map & Earn' (/bounties) from desktop More dropdown menu
- Layout: remove 'Map & Earn' (/bounties) from mobile slide-out nav
- Layout: clean up unused Gift icon import and stale isActive check
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Partners: removed all entries, now shows 4 placeholder slots
- Friends: added No Teeth Skateboarding with red transparent logo
linking to instagram.com/noteethskateboarding
- Friends grid: always shows real entries + fills remaining slots to 5
with "Coming soon" placeholders
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
1. AboutPage: add Credits section (Zapstore, ContextVM, Angor, Geyser, MKStack,
Shakespeare, Nostr), add Zapstore download button, fix powered-by text overflow
on mobile (wrap-friendly flex layout)
2. SasquatchGuide: make minimized bubble draggable on all devices
- pointer events for mouse, touch, and stylus
- saves position to localStorage, persists across sessions
- tap still opens, drag moves (5px threshold to distinguish)
3. Layout (burger menu): add overflow-y-auto + pb-8 to SheetContent so
mobile menu is scrollable when content exceeds screen height
4. CashuWalletCard (wallet): fix input+button rows overflowing on mobile
- flex-col on small screens, flex-row on xs+ (stacked on phones)
- whitespace-nowrap on buttons to prevent text wrapping
5. LabPage: compact action buttons on mobile (h-8, text-[10px], px-2)
so all 4 buttons fit in 2x2 grid without overflow/truncation
6. LoginDialog (Amber): improve visibility-change handler - use fresh
SimplePool subscription on each foreground return instead of querySync
(which doesn't exist in nostr-tools v2.13)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add visibilitychange listener: when app returns to foreground after Amber,
does a one-shot relay query to catch Amber's connect response
- Store pending nostrconnect session in localStorage so it survives app suspend
- Keep live relay subscription as fallback for web/PWA
- Add nsec login tab as reliable fallback option
- Add proper step states: idle -> connecting -> waiting -> success
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Replace NRelay1 async iterator (unreliable) with nostr-tools SimplePool.subscribeMany()
using onevent callback — same pattern as Ditto/Coracle/Nostrudel.
- Switch to relay.nsec.app, relay.damus.io, relay.primal.net (support ephemeral events)
- Use nip04.decrypt() from nostr-tools for Amber's connect response decryption
- Proper cleanup of pool and subscription on abort/close
- Fall back to signerPubkey as userPubkey if NConnectSigner.getPublicKey() fails
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Completely replaces the broken NIP-55 intent approach with NIP-46:
- Amber tab: generates nostrconnect:// URI, opens Amber, waits for
Amber to connect back via relay using NLogin.fromNostrConnect()
(same pattern as Ditto/soapbox-pub)
- Bunker tab: paste bunker:// URI from Amber or nsec.app
- Extension tab: NIP-07 browser extension (desktop)
- useLoginActions: updated to match Ditto's implementation with
nostrconnect(), generateNostrConnectParams, generateNostrConnectURI
- Clean mobile-first UI — no clipboard hacks, no intent tricks
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
NIP-55 (nostrsigner: intents) is fundamentally broken from Capacitor
WebView — no combination of window.open, window.location.href, or
anchor clicks works reliably across all Android WebView versions.
Switch to NIP-46 (bunker://) which works everywhere via WebSockets:
- User opens Amber → Connect apps → Add app → copies bunker:// URI
- Pastes it into the login dialog
- NLogin.fromBunker() completes the handshake over a relay
- Login works in browser, PWA, and Capacitor APK identically
This is exactly how Ditto (soapbox-pub) implements Amber login.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Amber has two code paths:
- Native path (no Browser.EXTRA_APPLICATION_ID): reads type from intent
EXTRAS — impossible to set from a WebView URL navigation
- Web/browser path (Browser.EXTRA_APPLICATION_ID set): reads type from
URL query params — triggered when WebView opens URL as browser intent
window.open(url, '_blank') causes Android WebView to fire a browser
ACTION_VIEW intent which sets Browser.EXTRA_APPLICATION_ID, routing
Amber to parse our nostrsigner: URL query params correctly.
Previous attempts:
- window.location.href: WebView intercepts as asset URL
- window.open(_system): strips query params
- anchor click: goes through native path (no extras = INVALID type)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
window.location.href causes WebView to try loading nostrsigner: as an
asset URL. window.open(_system) strips query params. The only reliable
method in Capacitor WebView is creating a temporary <a> element and
clicking it — this correctly triggers Android's intent system with the
full URL intact.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
window.open(_system) was stripping query parameters from the nostrsigner:
URL, causing Amber to receive a request with no type parameter and
rejecting it as malformed.
window.location.href works correctly in Capacitor WebView — Android
intercepts nostrsigner: scheme via intent-filter with full URL intact.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Capacitor native apps fail with "malformed nostrsigner request" when
using callbackUrl because Amber treats custom URI schemes as invalid.
Fix: detect window.Capacitor.isNativePlatform() and use the no-callback
clipboard flow (openAmberForLoginNoCallback) instead of the callbackUrl
flow. Amber copies the pubkey to clipboard, app reads it on
visibilitychange when user returns.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Remove debug red banner from index.html and main.tsx
- Add android:build:apk script that uses CAPACITOR_BUILD=1 for single
bundle output + strips Shakespeare build artifacts from dist/index.html
- Update all android:* scripts to use android:build:apk
Root cause of white screen: 100+ JS chunk files overwhelm Android
WebView local file serving. Single bundle with inlineDynamicImports
fixes this.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Red banner renders from pure HTML before any JS loads.
If visible → HTML loads but JS fails.
If not visible → WebView can't even load index.html.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Use window.Capacitor.Plugins.App directly instead of dynamic import()
so the build doesn't fail when @capacitor/app isn't in node_modules
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The primary cause of the white screen in the APK was BrowserRouter.
Capacitor serves files via https://localhost/ and the HTML5 History API
routing (BrowserRouter) fails to resolve routes in this context,
resulting in a blank white page.
Changes:
- Switch BrowserRouter → HashRouter in AppRouter.tsx
- Add DeepLinkHandler component (handles appUrlOpen for Amber callbacks
and other deep links in native context)
- Fix Amber callback URL builder to use org.skatespots.app:// custom
scheme when running in Capacitor (so Android fires an intent back into
the app instead of navigating a browser)
- Fix getSearchParams() in amber.ts to extract query params from hash
fragment (HashRouter URLs look like /#/amber-callback?event=XXX)
- Fix captureAmberCallback in main.tsx to check hash fragment too
- Remove debug error overlay (no longer needed)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add SplashScreen.hide() call in main.tsx so splash dismisses when JS loads
- Set launchShowDuration: 0 + launchAutoHide: true in capacitor.config.ts
- Fix ErrorBoundary to use bg-black text-white instead of bg-background
(bg-background = white before dark class is applied = white screen on error)
- These three combined should eliminate the white screen in the APK
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Amber login (critical):
- Replace window.location.href with window.open(_system) in Capacitor WebViews
- Capacitor's WebView throws "malformed nostrsigner request" on href navigation
- New openAmberUrl() helper detects Capacitor via window.Capacitor.isNativePlatform()
- Add openAmberForLoginNoCallback() for PWA/Capacitor clipboard flow
- Fix all 6 Amber URL opens in amber.ts + LoginDialog.tsx
Mobile nav:
- Newsletter and Sasquatch AI (/pyramid) added to hamburger menu
- Sasquatch floating guide now shows on mobile (removed hidden md:block)
Lab page mobile:
- Action buttons (Offer Coaching/Filming/Editing/Get Card) now 2-col grid on mobile
- Service type filter (Coaches/Filmers/Editors) now 3-col grid, full width
- Reduced top padding on mobile (py-6 → py-12 on md+)
- Text truncates cleanly, icons don't overflow
Wallet page mobile:
- Reduced padding (py-6 md:py-8)
- Tab triggers use h-auto + py-2.5 to avoid overflow
- Smaller header text on mobile
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Raise QUERY_TIMEOUT in useSkateSpots from 8s to 15s
- Raise AbortSignal.timeout in useLabProfiles and useSkateEvents from 10s to 15s
- Add useSkateRelay to AppConfigSchema so it's preserved when reading from localStorage
On a fresh phone (no warm WebSocket, no TanStack Query cache), the app
needs to: load JS bundle, boot React providers, open a WebSocket to
goskatespots.nostr1.com, and receive EOSE — all before the timeout.
On slow mobile connections this easily exceeded the old 8-10s limits,
causing /home to show empty sections while /map worked (the connection
was already warm by then). 15s gives cold connections enough headroom.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- "Sponsors see the organic number" -> "Users can now see both numbers"
- PublishToNostrButton now opens a preview modal first (NostrPreviewModal):
- Mock Nostr post card: avatar, display name, timestamp, post body
- nostrText rendered with hashtags in blue and URL underlined
- Fake engagement bar (replies/reposts/zaps at 0) for visual context
- Character count shown below the card
- Two actions: "Edit first" (closes modal) / "Publish" (confirms)
- Full-width on mobile, correct safe-area handling via Dialog component
- isPending state flows through so button shows "Publishing..." while signing
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- "Issue" replaced with "Drop" everywhere in UI and copy:
Drop #001, Weekly Drops, Write Drop #002, next drop, etc.
- nostrText: hashtags consolidated to one line (some clients break
each hashtag onto its own line when separated by newlines)
- nostrText: removed "Full issue:" prefix — bare URL unfurls cleaner
- 6 hashtags added: #skateboarding #nostr #skatespots #spotr
#newsletter #skatespotsnews
- Publish button tags updated to include all 6 as t-tags
- Template comment updated to match new format
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Kind 1 notes render differently on every client (Damus, Primal, Amethyst,
Snort, etc). The fix: keep the kind 1 as a 5-line teaser that works as
plain text on any client, with the full formatted issue at the URL.
Issue #001 nostrText trimmed from 14 lines to 4.
Added template comment with rules for all future issues:
- Max 5 content lines before the URL
- No bullet lists
- URL always on its own line
- 3 hashtags max
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Page container: py-8 on mobile + pb-24 so content clears fixed bottom nav
- Masthead headline: single line (no br), text-4xl mobile / text-6xl sm+
- Issue header: vertical stack on all screens — date/badge row, large number,
vibe text, then publish button full-width on mobile / auto on sm+
- Stats grid: always 3 cols (clean 2-row layout on phone), larger text-2xl
numbers, text-[10px] labels readable on all Androids, shorter relay label
- Floating pill: max-w-[calc(100vw-1.5rem)] prevents overflow on narrow screens,
right-3 tighter margin, whitespace-nowrap on text, touch-manipulation +
active:scale-95 for native tap feel, correct bottom offset for iOS safe area
- Monday admin banner: unchanged, already stacks correctly
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Structured issue schema (NewsletterIssue):
- updates[]: typed update items with icon/title/tag/body
- spotOfTheWeek?: optional typed spot block
- communityNote: free-text community section
- nostrText: plain-text ready to post as kind 1
Live community stats block (LiveStatsBlock):
- Pulls from useGrowthStats on every page load
- Shows spots, shops, events, mappers, coaches, tours
- Trusted/active account counts shown below spots + mappers
- "Live · pulled from goskatespots.nostr1.com" attribution
Publish to Nostr button (PublishToNostrButton):
- Only visible to 2 admin pubkeys — nobody else sees it
- Publishes kind 1 with nostrText + tags (skateboarding, skatespots, nostr)
- Includes relay URL tag pointing to /newsletter
- One-shot: button becomes "Published to Nostr" checkmark after success
- Full error handling with toast
Issue template now enforces consistent sections every week:
1. Community by the Numbers (live stats)
2. What's New (typed update cards)
3. Spot of the Week (optional — shows placeholder when omitted)
4. Community note
5. Support / Geyser
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- NewsletterPage (/newsletter) — zine aesthetic, dark bg, neon accents
Issue #001 covers all 8 security measures shipped this week:
geohash validation, URL sanitization, content limits, WoT defaults,
trusted stats, OfficialBadge, marker clustering, DM phishing warning
- NewsletterOverlay — two overlays rendered globally via Layout:
1. Floating "Latest Drop" pill — shows for 7 days after each issue,
dismissible per-issue via localStorage, hides on /newsletter page
2. Monday admin reminder — only visible to 2 authorized pubkeys
(founder + second admin), shows every Monday, one-click opens
Shakespeare AI with prompt pre-filled to write the next issue,
dismisses for the rest of that Monday via localStorage
- Footer — Newsletter link added to desktop footer alongside License/Supporters
- AppRouter — /newsletter route added (lazy loaded)
To add Issue #002: prepend a new object to NEWSLETTER_ISSUES array
in NewsletterPage.tsx. The pill and Monday reminder auto-update.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
New useGrowthStats hook:
- Queries wss://goskatespots.nostr1.com directly (authoritative source)
- Fetches all 7 content types in parallel: spots, shops, events,
coaches, filmers, editors, tours
- Deduplicates addressable events (keep newest per pubkey+kind+d)
- Buckets events by month with gap-filling for continuous X axis
- Computes cumulative spots line chart data
- Extracts top 10 mappers by spot count
- 15 min stale time — stats don't need to be live
New useGeyserStats hook:
- Queries Geyser GraphQL for supporter count + total sats raised
- Reuses same API already used in SupportersPage
Rewritten CommunityStatsPage:
- Section 1: 8 hero stat cards (spots, shops, events, coaches,
filmers, editors, tours, mappers) with per-type accent colours
- Section 2: 4 charts (cumulative spots area, monthly stacked bar,
active mappers bar, content mix donut)
- Section 3: Top 10 mappers leaderboard with avatars + 🥇🥈🥉
- Section 4: Supporters banner with live Geyser count + sats raised
- Footer: data source + verifiability note
- Full skeleton loading for each section independently
- Error state for relay connectivity issues
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
user.metadata can be undefined when the profile hasn't loaded yet.
Changed all .metadata.x accesses to .metadata?.x to prevent crash.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- AppRouter: lazy load all 40+ pages (only Index is eager)
→ initial JS bundle no longer includes code for pages never visited
- MapPage: lazy load SpotMap/Leaflet (~150KB) behind Suspense
→ Leaflet only downloads when user navigates to /map
- useSkateSpots/useShops/useMarketplace/useTours:
→ timeout: 30s → 8s (fail fast, show error sooner)
→ staleTime: 30s → 5min (trust cache, fewer relay round-trips)
→ gcTime: 5min → 30min (keep data in memory longer)
→ refetchOnMount: 'always' → true (use cache when data is fresh)
- Remove console.log/warn/debug spam from production hooks
- vite.config: manual chunk splitting for leaflet, nostr-tools, bitcoin
libs, radix-ui, react-router — each chunk cached independently
by browser, only re-downloaded when that library updates
- vite.config: esbuild drops console.log/warn/debug in prod build
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Redesigned icon.svg: skateboard deck shape (rounded nose/tail, truck axles,
wheels) beneath the existing map pin, all using the same red→orange→lime
gradient as the original
- Replace MapPin component icons in desktop header, mobile header, and footer
with the actual icon.svg image so the new logo shows everywhere
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Install @fontsource/bebas-neue and @fontsource/barlow-condensed
- Add .font-display (Bebas Neue) and .font-condensed (Barlow Condensed) CSS utilities
- Hero headline: split into two lines SKATE / SPOTS with clamp() fluid sizing,
Bebas Neue replaces Impact throughout
- Subtitle: Barlow Condensed 700 with wide letter-spacing
- Description: Barlow Condensed italic
- All section headings (Featured Spots, Events, The Lab, CTA, New Here?): Bebas Neue
- Section subheadings: Barlow Condensed with tracked uppercase
- Desktop + mobile header logo: Bebas Neue
- Footer logo: Bebas Neue
- Tagline badge: Barlow Condensed 600
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root causes found:
1. syncedUsers blocked re-sync permanently even on failure — first attempt
fires before WebSocket connections are ready on mobile, fails silently,
gets marked done, profile stays anonymous forever that session
2. useAuthor had 5min staleTime — empty {} result from pre-login query
was cached and not refetched after login
3. NostrSync did not force-remove the stale useAuthor cache entry
Fixes:
- Rewrite NostrSync with proper retry logic (up to 5 attempts, backoff)
- Tracks success/failure per pubkey, only stops on confirmed success
- Re-triggers on user.pubkey change (catches Amber polling completing)
- Forces queryClient.removeQueries on author cache before each attempt
- Tries Citrine → bootstrap relays → main pool in order
- useAuthor: reduce staleTime 5min→1min, expand fallback relay list
- Both files now use same BOOTSTRAP_RELAYS set for consistency
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Bug 1 - AppProvider was wiping NIP-65 relay list:
- Relay migration check was comparing stored relays vs defaults and
resetting them if they differed — overwriting the user's own NIP-65 list
- Fix: only reset relays to defaults if updatedAt === 0 (never synced)
and relays array is empty
Bug 2 - NostrSync not finding profile on mobile:
- Add Citrine local relay detection (ws://localhost:4869)
- If Citrine is running, query it first — instant local data
- Add BOOTSTRAP_RELAYS (purplepag.es, relay.nostr.band, nos.lol)
- Always try these for profile+relays before main pool
- syncRelays() now accepts optional relay group param
- Increase initial delay to 1500ms in PWA mode (800ms in browser)
to give WebSocket connections time to establish after Amber redirect
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
PWA standalone mode is single-tab. Opening nostrsigner: with a callbackUrl
sends the callback to the browser (a different app), not back to the PWA.
The PWA tab is suspended and never sees the result.
Fix:
- Detect PWA standalone mode via matchMedia('(display-mode: standalone)')
- In PWA mode: open Amber WITHOUT a callbackUrl — Amber copies pubkey to
clipboard automatically when no callback is given
- Use visibilitychange to detect when user returns to PWA after approving
- Auto-read clipboard on return and complete login silently
- Manual "I approved" button now tries localStorage first (browser), then
falls back to clipboard read (PWA) — covers both contexts with one button
- Browser mode unchanged: localStorage cross-tab polling still works
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause: on GrapheneOS, Amber opens /amber-callback in a BRAND NEW TAB.
login.amber() was being called in that new tab's React instance, which has
no effect on the original tab where the user is waiting.
Fix:
- AmberCallbackPage now writes pubkey to localStorage('amber_pending_pubkey')
instead of calling login.amber() — a cross-tab signal
- LoginDialog polls for 'amber_pending_pubkey' every 400ms and calls
login.amber() itself in the correct React context
- Manual "I approved" button also reads the same key
- Removed reliance on getAmberLogin() which checked the wrong localStorage key
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Both were added in the last mobile optimization and caused touch scroll
to stop working on Android/GrapheneOS browsers:
- overscroll-behavior: none on html/body blocks native touch scroll
- * { -webkit-overflow-scrolling: touch } on every element interferes
with the browser's native scroll handling
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause: window.location.href navigates the tab away entirely on Android,
destroying sessionStorage. When Amber calls back to /amber-callback the login
completes there, but returning to the dialog the state is lost.
Fixes:
- Switch from sessionStorage to localStorage for amber_return_path and
amber_login_attempt_ts — survives tab navigation on Android
- Add 500ms polling loop in handleAmberLogin that checks getAmberLogin()
every 500ms — detects when AmberCallbackPage stored the pubkey and
auto-completes login without needing visibilitychange to fire
- Add "I approved — complete login" manual button in waiting state as
explicit fallback for stuck users (reads localStorage right away)
- AmberCallbackPage now uses localStorage for all persistence
- Clean up poll interval on dialog close and unmount
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Add dedicated /amber-callback route so Amber always has a stable return URL
- Handles get_public_key (login), sign_event, nip44/nip04 encrypt/decrypt
- Shows animated processing screen while completing, then redirects back
- Add subscribeToAmberReturn() helper in amber.ts using visibilitychange
- Auto-detects when user returns to the tab after cancelling in Amber
- Resets login UI to initial state gracefully
- Update buildCallbackUrl() to use /amber-callback instead of current page URL
- Rewrite Amber tab in LoginDialog:
- Remove 2-step clipboard paste flow entirely
- Single tap opens Amber, login completes automatically on return
- Waiting state shows animated dots + cancel button
- No manual pubkey entry needed
- Save return path in sessionStorage so AmberCallbackPage redirects user back
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- ChatText component renders markdown [label](url) and bare URLs as neon clickable links
- 👉 URL patterns also rendered as clickable links
- get_weather tool: real-time weather + skateability score via Open-Meteo
Ask "weather in Bangkok?" or "is it good to skate in Barcelona today?"
- Footer updated: "Powered by ContextVM & Nostr Relays Data" with ContextVM
linking to https://contextvm.org/s/e3564bb877...
- Weather geocodes city name via Nominatim, returns temp/conditions/wind/skateability
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Dark forest green + neon lime (#ccff00) theme matching app's THUG2 aesthetic
- Mobile: compact strip above bottom nav, no more full-width overlay
- Minimized: small round button with neon pulse, lives at bottom-left
- Jungle texture strips top/bottom, dark gradient background
- Chat bubbles: neon red for user, neon lime for Sasquatch
- Input styled to match dark app theme
- Neon glow effects, live dot indicator
- Works cleanly on all screen sizes
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The encrypted CVM request/response cycle (kind 21000/21001) was too unreliable
in the browser — timeouts on every call. Replace with direct Nostr relay queries
using nostr.group() which is already proven to work in the app. Same filtering
logic as the server. No dependency on CVM server being up for data queries.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Previous models (llama-3.1-8b, gemma-3-4b, mistral-7b) returned 404 unavailable.
Tested and confirmed working: openai/gpt-oss-20b:free and google/gemma-4-31b-it:free.
Keep llama-3.3-70b as quality option when not rate-limited.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Try 4 free OpenRouter models in order (rate limit resilience)
- Add greeting responses to static FAQ so hi/hello/hey never need AI
- Fix static FAQ to exact-match short words like "hi" before partial match
- Better error message explaining free tier rate limits
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
nostr-tools v2 SimplePool.subscribeMany() callbacks (onevent/oneose) no
longer fire reliably in browser context, causing the CVM client to always
time out. Switch to raw Relay.connect() which uses the v2 API correctly.
Race across all CVM relays and resolve on first valid response.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
CVM server fixes (cvm-server/server.ts):
- Use 'title' tag instead of 'name' to read spot names (app publishes 'title')
- Search now checks 'title' tag (not just 'name') for keyword matches
- City filter now also checks 'title' tag and event content
- Country filter also checks event content
- Features filter now checks 't' tags (app uses 't' not 'feature')
- Validate spots with 't: skatespot' tag (same rule as the app)
- Return 'location' field and proper features list in results
- Image parsing now supports both 'image' (legacy) and 'imeta' (NIP-92)
CreateSpotPage fix (src/pages/CreateSpotPage.tsx):
- Add reverse geocoding via Nominatim on spot submit
- Emit structured 'city' and 'country' tags so CVM can filter by them
- Auto-fill location field from geocoding if user left it blank
SasquatchGuide improvement (src/components/SasquatchGuide.tsx):
- Add data model notes to system prompt explaining tag structure
- Instruct AI to handle 0-result responses gracefully (encourage adding spots)
- Use limit:20+ for city/country searches
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Rich system prompt that explains the CVM architecture: it aggregates
across multiple relays, deduplicates, and filters by features inside
events (complex queries that plain Nostr filters can't do)
- Switched tool call format from fragile regex (TOOL:name {...}) to
clean JSON {"tool":"name","args":{...}} that's easier for the LLM
- More robust JSON parsing — scans the full AI response for the tool call
- Better second-pass prompt: AI formats results with bullet points + links
- Updated suggested questions to showcase real CVM queries
- CVM failure gives a clear message explaining what the server does
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Added auth.nostr1.com and wotr.relatr.xyz back as default relays.
All 5 relays are now the default for all new users:
- wss://auth.nostr1.com (auth)
- wss://wotr.relatr.xyz (web of trust)
- wss://relay.ditto.pub (general)
- wss://goskatespots.nostr1.com (community - toggleable)
- wss://purplepag.es (profiles - read only)
Updated across: App.tsx, NostrProvider.tsx, NostrSync.tsx, TestApp.tsx
Updated Settings page Default Relays info cards to show all 5 with descriptions.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The 3.1-8b:free model is no longer free on OpenRouter.
Switching to llama-3.3-70b:free which is available and actually
a much better model (70B vs 8B).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Removed:
- [NostrProvider] Routing query to read relays (fired on EVERY query)
- [NostrProvider] Publishing kind X to relays
- [NostrSync] Starting sync / querying profile / profile query returned / sync completed
- [NostrSync] Profile not found on main relays
- [NostrSync] Profile synced and cached
- [Shakespeare] Fetching / Model / Response status debug logs
Kept all warn/error logs for actual failure cases.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Replaced Shakespeare AI (paid, NIP-98) with OpenRouter free model
(meta-llama/llama-3.1-8b-instruct:free). No login required, no credits
needed - anyone can use Sasquatch AI for free.
- Removed useShakespeare hook dependency from SasquatchGuide
- Added direct callAI() function using OpenRouter API
- AI + CVM live data still works the same way
- Removed login nudge, updated placeholder and AI badge
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
proxy.shakespeare.diy was returning NetworkError on all requests.
Direct calls to ai.shakespeare.diy work fine from a real browser.
Kept debug logging to confirm correct URL and pubkey.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Removed from: useCVMClient, NostrSync, useLoggedInAccounts, useAuthor, SettingsPage
Replaced with relay.damus.io where a fallback was still needed.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Use goskatespots.nostr1.com as primary (matches CVM server config),
plus relay.nostr.band and relay.damus.io as fallbacks.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
All fetch calls to ai.shakespeare.diy now go through proxy.shakespeare.diy
so they work from inside the iframe.diy preview sandbox.
The NIP-98 token is still signed against the original URL (not the proxy
URL) so the server-side signature validation passes correctly.
Affects: sendChatMessage, sendStreamingMessage, getAvailableModels
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>