Audited the whole events flow end-to-end (create, hosting dashboard, discovery
grid, detail, RSVP, sponsors) and confirmed the wiring is coherent:
- Create → parse → display round-trips correctly for sponsors, tour/spot/crew/
musician links, entry fee, and featured listings
- Sponsor parser safely skips shop-coordinate `sponsor` tags (no name at pos 2),
so events and tour/shop sponsorships never collide
- Batch RSVP counts feed the grid; hosting dashboard + detail keep per-item counts
Fix surfaced by the audit: CreateEventPage let a host pick a start time in the
past, silently producing a dead event in the Past tab. Added a `min={today}` on
the date pickers and a submit guard (10-min grace) with a clear message.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The filter toolbar was already sticky (stays put while cards scroll under it).
This reclaims the wasted vertical space above it on phones: smaller title,
reduced top padding + margins, and the subtitle is hidden on mobile. Desktop
layout unchanged.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
UX (all participants):
- EventCard now shows a live status pill (In 2 days / HAPPENING NOW / ENDED) on
both the full and compact variants — the at-a-glance signal was previously
detail-page only
- EventCard shows a "Presented by <sponsor>" ribbon so paid sponsor placements
get real visibility in the discovery feed (not just after a click)
- Added a bottom legibility gradient so overlays stay readable on any cover
FIX (crash risk): EventsPage getTrustScore did `trustScores?.map.get` — since
useBatchTrustScores returns a plain Map (no `.map`), this threw TypeError once
scores loaded. Now reads the Map directly.
PERF (N+1 query storm): rendering the grid fired one RSVP query PER card
(~100 queries on a full page). Added useBatchEventRsvps — a single #a-filtered
query for all event coordinates — and thread goingCount into EventCard as a prop.
Cards without the prop (detail page, hosting dashboard) keep their own query.
Removed now-dead getRelativeTime/relativeTime/isUpcoming (superseded by the
EventCountdown pill).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- FIX: EventCountdown label never updated (memo ignored the interval tick) — now recomputes every 30s for both EventCountdown and useEventStatus
- Add loud cancelled / rescheduled banner on EventDetailPage derived from host updates; close RSVP + dim hero when cancelled so attendees don't show up to a dead jam
- Clean up non-standard NIP-52 `a` tags (['a', coord, '', ''] → ['a', coord]) in RSVP and host-update publishing
- Correct misleading kind comment in useEventRsvps
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Per-event og:title/description/image/url + twitter:summary_large_image via useSeoMeta
- Share description composes date + location + blurb; falls back to site card image
- handleShare now uses the canonical /#/event/... URL, richer text, and toast feedback on clipboard copy
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Immersive EventDetailPage hero: full-bleed cover with legibility gradient,
bold uppercase title, type pills, host/music-act chips, and a 3-up meta bar
(date block · time · where). Past events render dimmed.
- EventCountdown: live status pill — "IN 3 DAYS" / "IN 2H 14M" /
"HAPPENING NOW" (pulsing) / "ENDED", refreshing every 30s.
- Actions (edit/calendar/share) moved into a clean glass overlay on the cover.
- Event sponsors: EventSponsor type + parse/serialize (mirrors TourSponsor),
EventSponsors "Presented by" strip (title sponsor hero row + logo grid),
sponsor editor on CreateEventPage, and sponsor preservation on edit.
- Document event sponsor tag in NIP.md.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- AttendeeList: tabbed Going/Interested with faces + names + a "who you trust"
signal (reuses batch trust scores); replaces the plain avatar stack
- Event Updates (kind 7103): usePostEventUpdate + useEventUpdates; host-only
composer with update/reschedule/relocation/cancelled categories that p-tags
every RSVPer so they get notified; public feed on the event page
- EditEventPage: auto-post a reschedule/relocation update to notify attendees
when the host changes time or location
- Activity bell: new 'event-update' notification type for RSVPers
- EventsPage: "Hosting" tab (logged-in) with a HostingDashboard listing the
user's upcoming/past events, live RSVP counts, and quick edit/update actions
- Document Event Update kind 7103 in NIP.md
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- New src/lib/routing.ts: keyless OSRM client, travel modes, distance/skate-time formatters
- New RouteDialog: self-contained Leaflet map drawing the route from the user's
location to a spot/shop/event, with a mode toggle and distance/time summary
- Wire the "Directions" buttons on Spot/Shop/Event detail pages to route in-app
instead of opening Google/Apple Maps
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Audit finding: the crew form was one 1,100-line endless scroll with ~15
sections in a punishing order (logo dead-last, MLS chat coordinator sitting
inline with the crew name), only one validation gate, and no sense of
progress or what's optional.
Fix — split the flow by intent:
- Create mode: a guided 4-step wizard with a sticky progress bar, per-step
hints, Back/Continue + "Skip to end", and a sticky footer nav.
1. The Basics — logo, name, about, home turf (all you actually need)
2. Identity & Vibe — vibe tags, crew type
3. Squad & Spots — members/roles, home spots, open call
4. Extras — treasury, video, sponsor, links, band, NIP-05,
agent, and the advanced chat coordinator (reframed
in plain language, clearly skippable)
Only the crew name is gated, and only at step 1. Clickable step dots allow
jumping back freely.
- Edit mode: unchanged UX intent — renders every section flat on one page
(power users want it all at once).
All field state and the entire publish/handleSubmit logic are preserved
verbatim; sections were extracted into composable blocks with zero behavior
change. Softened the coordinator copy and the map-pin section so a first-timer
isn't scared off.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Part A — Sponsor picker:
- CrewSponsorPicker searches the shop directory and emits a 30017 coordinate
so founders select a sponsoring shop instead of pasting a raw coordinate.
- Wired into CrewForm, replacing the raw coordinate input.
Part B — Notifiable join-requests (kind 31969):
- New addressable join-request event p-tagged to founder + admins, making the
request→approve direction symmetric with invites (surfaced in the bell, not
buried in a DM inbox).
- useCrewInvites: useSendJoinRequest, useIncomingJoinRequests,
useApproveJoinRequest (founder adds member), useDismissJoinRequest (local).
- CrewRequestToJoin now publishes the structured event.
- JoinRequestCard (Add to crew / Dismiss) wired into NotificationBell and
counted in the unread badge.
- Documented kind 31969 in NIP.md alongside invites.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The crew coordinator is no longer just a pointer — when a crew configures one,
the Chat tab runs through it for real.
- src/lib/cordn: clean transport/crypto split so an MLS (ts-mls) upgrade is a
drop-in swap of the crypto layer only.
- types.ts: CordnMessage/CordnEndpoint + envelope kind 4497 + app tag.
- crypto.ts: sealGroupMessage/openGroupMessage — real NIP-44 group encryption
(per-recipient sealing), the exact seam MLS encrypt-to-epoch slots into.
- client.ts: buildEnvelope/publishEnvelope/fetchGroupMessages with per-group
cursor ordering and ciphertext→author sender binding (anti-spoof).
- endpoint.ts: resolve a crew's `coordinator` to a usable relay endpoint
(wss/ws direct; https falls back to the ContextVM relay).
- useCordnChat: fetch-then-poll hook mirroring the NIP-17 chat API.
- CrewChat: uses Cordn when the crew has a coordinator, NIP-17 fallback
otherwise; header badge shows the active transport (Cordn vs Private).
- CrewCoordinatorCard: reflects that private chat is live.
- NIP.md: document envelope kind 4497, delivery (fetch-then-poll + cursors),
the nip44-group sealed content, and the honest crypto→MLS seam.
Real encryption, real coordinator routing, non-breaking (fallback intact),
with the MLS ratchet as a clearly-marked next step.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
A skate team is more than "members" — the filmer, photographer and editor are
core functions. Crews can now assign optional, non-pretentious skate roles:
- Types: CrewRole (filmer/photographer/editor/artist/manager/og) + CREW_ROLE_META
labels/emoji, `crew-role` tag parser, and `crewRoles` on SkateCrew. Roles are
additive to membership and grant NO powers (only `admin` does); a member can
hold roles AND be an admin. Only honoured for founder/listed members.
- CollectiveMembers: optional rolesByPubkey → renders 🎥 Filmer / 📷 Photographer
flex chips next to each member.
- CrewLeadership: leader + captains show their skate-role chips.
- CrewForm: per-member role toggles + a founder ("Your roles") picker; publishes
`crew-role` tags scoped to the roster.
- CrewDetailPage: builds rolesByPubkey and threads it to members + leadership.
- NIP.md: document the `crew-role` tag + example.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
A skate crew is defined by its footage. Crews can now feature a headline video:
- videoEmbed.ts: safe parser accepting ONLY YouTube and Instagram URLs (full
parts on YT, clips on IG). Extracts video id / shortcode; rejects everything
else so we never render an untrusted iframe. No native/self-hosted video.
- CrewVideoSection ("The Part"): cinematic section — YouTube click-to-load
facade (poster + play button, no heavy iframe/cookies until play) and an
Instagram permalink embed, framed with skate language.
- SkateCrew gains `videoUrl` (tag `video`), validated through the embed parser.
- CrewForm: "The Part" input with live source detection (YouTube/Instagram) and
validation; publishes the `video` tag.
- Crews grid: crews with footage flex a "▶ Part" badge as a discovery signal.
- NIP.md: document the `video` tag (YouTube/Instagram only) + example.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Extend SkateCrew with `sponsorRef` (sponsoring shop, NIP-15 stall coord) and
`coordinator` (Cordn/MLS encrypted-chat Delivery Service pointer), plus parser.
- CrewSponsorBanner: "Sponsored by [Shop]" banner that resolves and links the
backing shop; renders nothing if the shop can't be resolved.
- CrewCoordinatorCard: surfaces the crew's private-chat coordinator (E2E · MLS),
explains what the coordinator can/can't see, shows the operator, and prompts
the leader to set one up when absent — the UI foundation for Cordn chat.
- Wire the sponsor banner (under hero) and coordinator card (sidebar) into
CrewDetailPage.
- CrewForm: add "Sponsored by" and "Private Chat Coordinator" inputs with
validation; publish `sponsor` and `coordinator` tags.
- NIP.md: document the `sponsor` and `coordinator` tags, the structured invite
kind 36229 (with accept/decline flow), and the Cordn/MLS private-chat model
and why it's the chosen fit for crews.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- New CrewHero: immersive full-bleed banner with per-crew accent glow, club-crest
logo overlapping the banner, member-count crest, big team name with neon shadow,
reputation crown + live activity pulse, focus/verification chips, "Led by"
team-leader inline, vibes and bio. Actions are passed in from the page.
- New CrewLeadership card: frames the founder as "Team Leader" and admins as
"Captains" with role crests, verified ticks and quick-DM — establishing the
crew hierarchy and laying groundwork for coordinator roles.
- Rewire CrewDetailPage to use CrewHero + CrewLeadership; wire live activity via
useCrewsActivity and confirmed-member/admin sets. Removed the old flat hero
card and now-unused imports.
- Upgrade CrewsPage header into a proper hero band with gradient, glow and
stat pills.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Upgrade CrewInviteDialog with a "Send invite" tab: invite a specific npub
with live invitee preview, optional note, and founder-only "invite as admin"
toggle. Preserves the shareable-link flow on a second tab.
- Add NotificationBell to desktop + mobile headers: unread badge, popover with
incoming crew invites (accept/decline) and pending-confirmation crews
(confirm with public/crew-only visibility).
- Wire useSendCrewInvite / useMyCrewInvites / accept / decline hooks and the
useCrewNotifications aggregator into the UI.
- Membership stays unforgeable (founder p-tag + invitee NIP-32 attestation).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Move sponsors from a buried sidebar card to a prominent full-width "Backed by"
bar right under the stats strip, above the About/content grid
- Redesigned as a horizontal pill row with an Award badge + gradient/glow accent,
so reppin' brands reads as a badge of pride
- Removed the duplicate sidebar Sponsors card
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- Even 2x2 (mobile) / 1x4 (desktop) grid instead of a ragged flex-wrap where
"Gear" dropped to its own row and "Member since" floated awkwardly
- Each stat now has an icon badge + prominent count + label, so the number reads
as a number (previously the tiny colored count looked like a status dot)
- Zero-value stats render in a muted, inert state — a brand-new profile now reads
as "nothing yet" rather than a wall of dead grey zeros
- Moved "Zaps received" + "Member since" into a clean secondary meta line below,
keeping the primary grid focused on the four core contributions
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- StatPill now accepts an onClick and renders as a hover-lift button when it has
a non-zero value (inert div when empty), with clear affordance
- Spots / Shops / Crews / Gear pills smooth-scroll to their matching section
(added ids + scroll-mt-24 offset so headings aren't hidden under sticky nav)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
NIP-05 (the field you couldn't find — it existed but was a bare input):
- Promote to its own prominent, self-explaining row (out of the cramped grid)
- Live verification: checks the handle resolves back to your own key, with checking/verified/mismatch/unreachable states, an inline status icon, plain-language copy, and a "How?" help link
Share + QR:
- New ShareProfileDialog: QR of the profile URL (great for IRL/stickers), copy-link, and native share-sheet fallback
- "Share" button added to every profile header
At-a-glance stats:
- Compact stat strip near the top (Spots / Shops / Crews / Gear / Zaps + "Member since")
- Removed the now-redundant sidebar "Contributions" card
Cleaner empty states:
- Spots/Shops sections no longer show dead "nothing here" cards on other people's profiles — hidden entirely when empty
- Own empty Spots state is now an actionable nudge linking to /create-spot
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
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>
- 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>
- 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>
- 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>
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>
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>