Commit graph

317 commits

Author SHA1 Message Date
SkateSpots
6746353670 feat: add Transport Lab craft with self-attested fleet profiles 2026-10-29 22:34:28 +07:00
SkateSpots
a2ab5750ff feat: Cordn inbox, outbox, room controls and animated splash 2026-10-29 19:18:48 +07:00
SkateSpots
89e6438ee1 fix: make community joins self-healing and keep /c in the app 2026-10-29 13:27:12 +07:00
SkateSpots
673bb983b8 feat: per-thread DM mute, event pass, a11y header, undo mute 2026-10-29 12:37:10 +07:00
SkateSpots
4ae6b1d5ea feat: envelope-only DM unread in the bell 2026-10-28 22:43:52 +07:00
SkateSpots
534e939f9e feat: global command palette and scroll/layer context 2026-10-28 18:08:45 +07:00
SkateSpots
d741823039 feat: Android back/app links and web app-shell cache 2026-10-28 16:39:07 +07:00
SkateSpots
075aea7f44 perf: lazy content images and targeted crew member queries 2026-10-28 13:11:10 +07:00
SkateSpots
e94c16a479 perf: batch profiles, gate map layers, warm public cache 2026-10-28 12:42:39 +07:00
SkateSpots
66fc47cc28 feat: restore map footer, inline crew roles, Cordn reactions/push
- Keep the map footer links permanently visible; remove the hide toggle.
- Publish inline founder promote/demote from the member row.
- Add encrypted MLS reaction/delete/read control messages and UI.
- Add the minute Cordn join watcher: NIP-17 DMs + encrypted bell notices.
- Add the browser translation-matrix runner and a low-end performance finding.
2026-10-28 11:02:47 +07:00
SkateSpots
f029b5a516 feat(newsletter): weekly generated drafts, covers, and admin preview
- Add a browser-safe editorial generator with rotating satire and shipped highlights.
- Generate a fresh 1200x630 cover from the Drop #006 foundation and upload it to Blossom.
- Publish weekly drafts as NIP-78; add the admin Load draft/Preview composer flow and banner.
- Add the Monday user timer (draft by default) and a manual newsletter:weekly script.
- Remove static/live issue duplicates; translate the new admin strings to all 24 locales.
2026-10-28 09:42:44 +07:00
SkateSpots
630780753f feat(cordn): device QR, member removal, message reply; patch deps 2026-10-28 09:14:03 +07:00
SkateSpots
7da67cc2c0 feat(cordn): unread and join-request badges; persist pending joins 2026-10-28 08:39:01 +07:00
SkateSpots
2ec861befd feat(community): add the app-wide Cordn room and always-on bot 2026-10-27 17:37:09 +07:00
SkateSpots
74249e304e fix(menus): move language into its own mobile view; fit crew tabs 2026-10-27 17:00:19 +07:00
SkateSpots
8a5ac47a19 fix(translation): repair Basque tokenizer IDs and evict stale cache 2026-10-27 16:32:29 +07:00
SkateSpots
e85b9aacb6 feat(chat): all-language on-device translation (M2M100 + Basque Opus)
- M2M100 418M (MIT, 617 MB q8) covers 23 app languages; Basque uses converted
  Opus eu-en/en-eu q8 packs; all self-hosted under relay.skate-spots.org/models.
- Worker uses M2M100 directly and pivots Basque through English; UI targets all
  languages; old pilot restriction/note removed; memory-only + guard unchanged.
- Verified en<->es, ca<->gl, hi<->en from the host. Basque packs are converted
  and hosted; the final MarianTokenizer/ONNX runtime check is still pending.
- 138 tests, 24 locales, build and smoke green. SpotChat deliberately skipped.
2026-10-27 13:06:04 +07:00
SkateSpots
67bc89861d fix(chat): clear translation memory + worker on logout
Translations are memory-only; on logout/account switch we now clear the cache,
reject pending requests and terminate the worker (the model itself is public
and stays in the browser cache).
2026-10-27 10:52:01 +07:00
SkateSpots
d2a24679f0 feat(chat): on-device translation pilot (English ↔ Spanish)
- @xenova/transformers 2.17.2 in a Web Worker; Opus-MT en↔es ONNX q8 packs
  self-hosted on the relay VPS (/models + CORS); ONNX Runtime 1.14 WASM
  self-hosted in public/ort/ (37MB) loaded from ../ort/.
- Hard network guard: only model GET/HEAD (no query) and /ort/ assets allowed.
  Message text travels only via postMessage; 9 tests prove fetch is never called
  with the text and every other request is blocked.
- TranslateMessage in DMs + crew chat; Settings auto-translate off by default;
  memory-only translations; per-model download progress; 12 UI strings in all
  24 locales.
- Both model directions verified from the hosted files; tsc, 138 tests, build
  and smoke green.
2026-10-27 10:49:54 +07:00
SkateSpots
8c7de4d86d feat(chat): on-device language detector (translation Phase 1)
- tinyld (MIT, lazy, local) + script rules + a weighted
  Catalan/Galician/Basque/Spanish/Portuguese rule for the close/short cases
  tinyld confuses. No network, no logs, no storage; conversation/contact prior
  supported.
- Lingua with 24 languages produced a 99 MB WASM, so it was rejected as a
  detector; tinyld + the gl rule covers all 24 at a fraction of the size.
- 28/28 language samples pass, including a test that fetch is never called.
- Phase 2: on-device Opus-MT (English pivot) in a worker, memory-only
  translations, model packs on Blossom, sentinel/network-guard test.
2026-10-27 10:30:50 +07:00
SkateSpots
828684cf15 feat(i18n): add Hindi (24 dictionaries, 25 codes, all 100%)
- Full 8,703-key Hindi dictionary in src/i18n/hi (7 namespace batches),
  placeholders verified, only 2% identical strings (brand/protocol names).
- Register hi in Language/LANGUAGE_LOADERS, detectLanguage (hi-IN),
  LanguageToggle (🇮🇳, हिन्दी) and dateFnsI18n (hi-IN).
- Lazy-load Noto Sans Devanagari for hi via lib/localeFonts.ts (renamed from
  cjkFonts.ts) and add it to the CSS font stack; other locales don't download it.
- check-i18n covers hi: 24 dictionaries / 25 codes at 100%; tsc/tests/build green.
2026-10-26 16:35:53 +07:00
SkateSpots
7e48884eb9 feat(map,i18n): single-copy world clamp + regional flags + pt-PT
- Map: min zoom = smallest integer zoom whose 256*2^z world covers the
  viewport (no grey bands, no duplicated continents). Horizontal wrap is
  restored (worldCopyJump, no longitude maxBounds) so Hawaii/Pacific are
  reachable by dragging; vertical panning stays clamped.
- Flags: real SVGs for Catalonia, Basque Country, Galicia and England
  (no Unicode region flags exist; English is not the UK). Portuguese now has
  Brasil (pt) and Portugal (pt-PT).
- pt-PT shares the pt dictionary with a European drift for the visible chrome
  (Guardar, Palavra-passe, Autocarro, Utilizador, ...).
- check-i18n covers pt-PT: 24 codes at 100%; tsc/tests/build green.
2026-10-26 16:09:21 +07:00
SkateSpots
d4c6261b81 feat(i18n): add Catalan, Basque and Galician (23 locales, all 100%)
- Full dictionaries (8,703 keys each) in src/i18n/{ca,eu,gl}, 7 namespace
  batches, composed with deepMerge; placeholders verified, 2-4% identical
  strings are only brand/protocol names.
- Register in Language/LANGUAGE_LOADERS, detectLanguage (before es),
  LanguageToggle (CA/EU/GL text badges - no Unicode region flags exist) and
  dateFnsI18n (ca-ES/eu-ES/gl-ES).
- check-i18n.mjs now covers ca/eu/gl: all 23 locales at 100%, tsc/tests/build
  green.
2026-10-26 15:56:56 +07:00
SkateSpots
87f58cd84e feat(android): pass-the-app card (QR + share sheet + NFC tag)
About page renders ShareAppCard on Android only: a QR of the Zapstore listing,
a share-sheet button for the install link, and NfcWriteButton to write it to a
tag. Single-sourced APP_INSTALL_URL in lib/platform.ts (also used by the About
download button). No peer-to-peer APK transfer (needs native Wi-Fi Direct/BLE +
PackageInstaller + device QA); Zapstore handles install/update.
2026-10-26 11:41:51 +07:00
SkateSpots
cc7eb58828 fix(amber): disable result compression (native) + decode Signer1 fallback
The web/PWA Amber path set compressionType=none; the native intents set neither
compressionType nor returnType, and the JS gzip decoder was never called. NIP-55
lets Amber gzip a large result (Signer1... base64), so a long nip44_decrypt (a
long DM) could arrive compressed and be parsed as garbage. Native intents now
send compressionType=none; the JS signer also gunzips any Signer1 result. Timeout
message no longer wrongly says Amber may not be installed.
2026-10-13 21:05:29 +07:00
SkateSpots
10299d07f2 perf(dm): persist the decrypt cache so each message decrypts once, ever
The last Amber pain: a fresh launch re-decrypted the whole inbox (2 signer ops
per wrap). Decrypted messages are now persisted to IndexedDB
(src/lib/dmDecryptCache.ts, keyed viewerPk:wrapId, bounded 4000/account, pruned
oldest-first) and hydrated once per session before the first lookup, so a wrap is
decrypted at most once ever. Cleared per-account on logout (plaintext at rest
otherwise readable by another account on the device).
2026-10-13 18:58:35 +07:00
SkateSpots
27084a4e7b fix(amber): send crew messages one signer intent at a time
Sending to a crew fired every recipient's gift wrap in parallel (Promise.all),
and the sealed-relay crew chat encrypted to the whole roster in parallel. Amber
is a single foreground activity: concurrent nostrsigner: intents mean only one is
answered and the rest sit until the 60s native timeout rejects with 'Amber didn't
respond'. Both paths now go sequentially for an interactive signer (parallel kept
for silent signers). Also corrected the approval hint (2 per member: encrypt +
sign, not 1 per recipient).
2026-10-13 00:06:49 +07:00
SkateSpots
4bdb949f06 feat(messages): Crews section in the Messages hub -> each crew's private chat
- DirectMessagesPage gains a 'Crews' block listing the viewer's crews with an
  Open/Set-up badge (from one local IndexedDB read via useCordnGroupGids),
  linking into /crew/<pk>/<id>?tab=mls.
- Crew page tabs are now URL-addressable (?tab=events|chat|mls); an explicit
  param wins over the auto-default. useUserCrews + cordnGroupGids added.
2026-10-12 23:35:35 +07:00
SkateSpots
a2bc42c94b fix(onboarding): stop the first-run checklist + backup reminder nagging returning users
- FirstRunChecklist showed to any logged-in user until manually dismissed; it now
  requires a recent signup_completed stamp AND first-run-seen (auto, once), so
  established/imported accounts never see it.
- SignupDialog wrote the backup-confirmed flag under user?.pubkey while the login
  was still settling, so it landed on 'backup-confirmed:anon' and the post-signup
  BackupReminder nagged every session. Now markBackupConfirmed() is called with the
  pubkey derived from the minted nsec.
- NIP-49 export password field autoComplete new-password -> off (fewer browser
  save-password prompts).
2026-10-12 23:11:42 +07:00
SkateSpots
bcc3eceeee Cordn native: default crew tab to the private chat + founder nudge; release 1.6.1
- Crew page defaults to the Private-chat (MLS) tab when this device already has
  the group (useCordnGroupExists reads the IndexedDB snapshot — local only, no
  signer prompt); a manual tab click always wins.
- Founder/admin banner ('Set up') points at the tab when the crew has no group.
- Bump 1.6.0 -> 1.6.1 and add release notes for everything since 1.6.0 (private
  crew chat/MLS, self-hosted relay + media, marketplace close-the-deal, trust
  controls, map/weather, Clothing Designer, UI fixes).
2026-10-12 22:52:33 +07:00
SkateSpots
a9a5966714 Cordn native: browser probe, group-ref sharing, image messages
- scripts/cordn-browser-probe.mjs: headless-Chromium proof that the built
  native chunk runs in a real browser — imports the chunk, runs a session
  against the coordinator (publish kp, create group, send), then reopens from
  the Dexie/IndexedDB store. Not part of npm test (needs network + Chromium).
- Group ref: the panel shows/copies the group's cordn1... ref
  (encodeGroupRef from the crew coordinate + coordinator), so it can be opened
  in Staircase/cordn-web. Computed in the hook via the lazy native import.
- Images: attach button uploads to Blossom (useUploadFile) and sends the URL
  inside the encrypted MLS envelope (['image', url]); CordnChatMessage.imageUrl
  + ImageWithFallback rendering. Covered by the live test.
- Unread badge deliberately not built (needs a background signed request).

npm test + smoke + browser probe + 3 live scenarios all pass.
2026-10-12 22:42:44 +07:00
SkateSpots
383fb07127 Cordn native UI: crew MLS chat tab, IndexedDB store, membership flow
Milestone 2 for the native Cordn client.

- Session now runs on the viewer's own signer (NIP-07/Amber/nsec) instead of a
  raw key, so the Cordn identity is the user's pubkey. Added idbStore.ts
  (Dexie 'SkateSpotsCordnDB', one snapshot per account) so MLS state survives a
  reload, plus membership delivery: requestJoin / fetchJoinRequests /
  fetchWelcomes / acceptWelcome / addMemberByIdentifier, and addMember now also
  stores the Welcome on the coordinator for the target.
- useCordnNative owns the session lifecycle and lazy-imports the native module,
  keeping @contextvm/sdk + ts-mls in a separate ~678 kB async chunk (entry
  bundle unchanged at 1.9 MB).
- CordnNativePanel: a third 'Private chat' tab on the crew page — founder/admin
  create the group and approve join requests; members ask to join and check for
  an invite; live message list + composer (polling only for silent signers).
- Strings in i18n/legacy/cordnUi.ts (en/es).

Verified live against our coordinator with a new scenario
(join-request -> approve -> welcome) plus the existing two; npm test + smoke
pass. Not yet exercised in a real browser (browser tooling was offline this
session) — the dynamic-import failure path degrades to an inline error.
2026-10-12 22:30:15 +07:00
SkateSpots
1dcaf8cea3 Cordn native: real MLS client (Level 2 foundation)
Add src/lib/cordn/native/ — a browser-safe Cordn client doing real RFC 9420
MLS over a ContextVM coordinator, mirroring Cordn's wire contract (reimplemented,
not copied from @cordn/cli).

- mls.ts: identity, key packages, group create/add/join, application messages,
  the ChaCha20-Poly1305 coordinator wrapper, causal prev tags.
- coordinator.ts: ContextVM/MCP-over-Nostr client on @contextvm/sdk, validated
  with @cordn/core schemas.
- session.ts: CordnSession (key packages, createGroup, addMember->Welcome,
  joinGroup, send, fetch) with persistence behind an injectable CordnStore
  (MemoryCordnStore today; IndexedDB adapter TODO).
- Verified end-to-end against our own coordinator: a two-member round trip
  (CORDN_LIVE=1 vitest run native.live.test.ts) — create, add, join, and
  bidirectional encrypted messages with proven senders.

Deps: @cordn/core, @contextvm/mcp-sdk, @noble/ciphers pinned 2.2.0 (fixes the
previously invalid tree: ts-mls wants 2.2.0, a stale root copy was 0.5.3).
src/test/setup.ts is now guarded for node-environment test files. Not wired to
any UI yet; nothing imports the module, so the web bundle is unchanged.
2026-10-12 22:14:20 +07:00
SkateSpots
9251aea5b1 feat(cordn): crew Cordn group link-out (Level 1)
Crews can carry a Cordn group ref (['cordn', <cordn1-ref-or-url>] -> SkateCrew.cordnGroup): CrewForm gains a 'Cordn group' field and CrewCoordinatorCard shows an 'Open crew chat' (cordn.net/chat/<ref>) + 'Copy for Staircase' row. Verified a real ContextVM client (@contextvm/sdk) connects + lists tools against our self-hosted coordinator.
2026-10-12 21:46:03 +07:00
SkateSpots
cc250b4d0b perf(fonts): lazy-load CJK fonts per locale (~16MB font payload off the boot path)
Kanit (th), Noto Sans JP (ja) and Noto Sans KR (ko) were imported statically in main.tsx, so every visitor downloaded ~16MB of CJK font files. They're now loaded on demand once per locale (lib/cjkFonts.ts, wired in LanguageContext), so non-CJK users never fetch them and CJK users get their font after the first paint.
2026-10-12 19:52:54 +07:00
SkateSpots
a8e411faa0 feat(cordn): embedded coordinator on the relay, crews default to it, disclosure
Newlay runs its embedded Cordn coordinator (identity cordn.key, uid 10001). SKATESPOTS_COORDINATOR is the provided default: Settings -> Coordinator shows a 'Use SkateSpots coordinator' block and CrewForm prefills it for new crews; BYO coordinator stays client-side. Disclosure copy (coordinators see ciphertext + metadata, never contents) in socialUi.coordinator.ourDefault* + existing privacyNote.
2026-10-12 16:36:56 +07:00
SkateSpots
54882285b6 feat(weather): 6-day forecast
FORECAST_DAYS=6 in lib/weather.ts is the single source for the fetch length and the UI labels (spot card + map popup). New count-interpolated labels spotUi.forecastDays / mapUi.forecastDays (en/es; other locales fall back to English). Legend colours moved to lib/skateabilityLevels.ts to keep the react-refresh lint at zero.
2026-10-12 13:51:29 +07:00
SkateSpots
e47c4b9d81 feat(weather): legend, refresh/error states, sunrise/sunset, reason chips
- WeatherLegend overlay on the map (only while the weather layer is on), colours mirroring calculateSkateability
- Spot weather card: freshness line + manual refresh, and an error/retry state when Open-Meteo is unreachable
- Sunrise/sunset row (weather.ts now fetches Open-Meteo daily sunrise,sunset)
- Score reasons render as chips, not a bullet-joined run-on
- Map popup muted text gray-500 -> gray-300 for contrast
Units stay Celsius/km/h (deliberate).
2026-10-12 13:44:19 +07:00
SkateSpots
84c0ba7b3c feat(relays): ensure the community relay is in the NIP-65 list (+ silent republish)
lib/relays gains withCommunityRelay/ensureCommunityRelay; AppProvider and NostrSync add wss://relay.skate-spots.org to a stored list that lacks it (unless useSkateRelay is off). NostrSync republishes kind-10002 once for non-Amber signers so other clients pick up the community relay; Amber users keep it local to avoid an unprompted popup.
2026-10-12 13:29:04 +07:00
SkateSpots
37cc0d5d68 fix(relays): prune retired/dead relays from stored lists (runtime + load + sync)
Adds RETIRED_RELAY_HOSTS (goskatespots.nostr1.com, wotr.relatr.xyz) with isRetiredRelay/pruneRetiredRelays. Applied in resolveRelays (runtime), AppProvider (one-time cleanup on load) and NostrSync (when applying a fetched NIP-65), so existing users stop showing/using the retired relay without manual edits.
2026-10-12 13:20:09 +07:00
SkateSpots
7611f3d266 feat(media): self-host Blossom at blossom.skate-spots.org (primary upload target)
Newlay's Blossom enabled on the relay box (blobs inside the data volume); Caddy + DNS + TLS wired. The app now uploads there first, keeping blossom.primal.net and blossom.band as mirrors, and includes it in BLOSSOM_MIRROR_HOSTS for read fallback.
2026-10-12 13:12:14 +07:00
SkateSpots
6407310213 feat(relay): cut over community relay to self-hosted Newlay
Point SKATE_RELAY, the default relay pools, the DM/CVM/zap relay arrays, the cvm-server + push-service defaults, and the archive default at wss://relay.skate-spots.org, keeping the legacy hosted relay as a fallback during the client-update window. Restored 115/115 archived events; the relay runs behind Caddy with TLS. Docs + AGENTS.md updated.
2026-10-12 12:38:13 +07:00
SkateSpots
0b645d6d67 feat(lab): add Clothing Designer craft (kind 35015)
New data-driven Lab craft, mirroring the artist role: three skill
sections (What You Make [required] / How You Make It / Materials) plus
minimum order, turnaround, size range, samples and customisation extras,
inheriting the shared Lab extras (rate, payment, gallery, reel,
languages, availability, booking).

Wired through types.ts (kind, label/description maps, parse, collective-
capable), labelKeys, useLabProfiles (kind list + dispatch), becomeRoles
(role card), craftFields, LabPage (tab, filters, offer button, empty
state, counts, deep links), LabCrewsStrip, NIP.md, and en/es i18n
(labOptions, becomeRolesUi, labUi). /become/clothing added to the smoke
route list. tsc, eslint, check-i18n, vitest 101/101, build and smoke all
pass.
2026-10-10 19:30:47 +07:00
SkateSpots
7a5ec82f84 fix(marketplace): escrow-required guard, ask-seller on active, scoped receipts
Correctness batch from MARKETPLACE_FLOW_AUDIT.md:
- canBuy is disabled when the seller requires escrow (item.escrow ===
  'required'), so a direct Buy-with-sats cannot bypass it.
- ask-seller (31872) is available on an active listing, not only once sold,
  so a buyer who paid off-app is no longer at a dead end.
- payment receipts (lib/receipts.ts) are scoped to the payer's pubkey, so a
  stale or foreign receipt cannot be attached to a trade by another account
  viewing the same listing.
2026-10-10 09:08:52 +07:00
SkateSpots
ac1b22674c fix(escrow): show and choose the mint, block non-P2PK mints
The escrow mint was silently the buyer's preferred/first trusted mint
(useCashuEscrow: prefs.preferredMintUrl || mints[0].url) with no UI, even
though the seller redeems there. Now:
- the escrow dialog shows the mint and lets the buyer pick from their
  trusted mints (defaults to their preference);
- a mint whose NUT-06 info advertises no NUT-11 (P2PK) support is blocked
  with a clear message before minting (cashuEscrow.mintSupportsP2PK);
- the create call uses the selected mint.
Still open (scoped separately): consult a seller-advertised mint and
seller-side receive from an unfamiliar mint. Recorded in the audit.
2026-10-09 23:07:24 +07:00
SkateSpots
077fb2d5d1 fix(marketplace): payment, attestation, and escrow safety
Next batch from MARKETPLACE_FLOW_AUDIT.md:
- bolt11.ts: getBolt11Expiry / isBolt11Expired; usePayPaymentPointer refuses
  an expired invoice before paying (a lapsed offer invoice used to fail deep
  in the wallet with a confusing error).
- PayWithSats shows unsupported pointer kinds (creq/clink) as unavailable up
  front instead of enabling a button that fails after the click.
- useTradeAttestations: verify events (onlyVerified) and keep the newest per
  (author, kind, d), so a newer s=revoked supersedes a stale valid copy;
  useTradeReputation no longer trusts unverified relay events.
- EscrowRequestDialog persists the minted token locally before publishing
  (lib/pendingEscrow.ts), so a failed send cannot strand the locked funds.
2026-10-09 23:00:35 +07:00
SkateSpots
e66888128f feat(trust): a real mute (NIP-51 kind 10000) that hides content
Completes the safety loop. useMuteList reads the viewer's kind-10000 mute
list plus optimistic local mutes (lib/localMutes.ts); useMuteUser
republishes the list with the target added, records it locally and
invalidates content queries. useAdminModeration carries a new
mutedPubkeys set, and the shared filterAdminHidden/filterAdminHiddenMixed
drop muted authors — so every list hook filters them with no per-hook
changes (admin tooling with includeHidden still sees them). The flagged
notice gains a Mute action.
2026-10-09 18:23:04 +07:00
SkateSpots
2d1144bb7e feat(trust): batch provider reads, neutral direct follows, flagged notice
Second pass of TRUST_SCORE.md.

- useTrustAssertions now batches: every TrustBadge registers its subject into
  a shared per-viewer union and ONE query fetches the union (no per-card
  query), with a debounced refetch when a list grows. Same hook API.
- A direct follow (or self) is no longer shown as Verified - only a
  friend-of-friend or a provider assertion counts; Flagged is never
  suppressed.
- useUserReports reads public NIP-56 reports for an account, and
  TrustFlaggedNotice shows a flagged counterparty with the reporters grouped
  by the viewer's network plus Report / Dismiss. Wired into the listing
  seller card and the coach detail page.
- New walletUi.trust.* keys (en/es).
2026-10-09 17:17:11 +07:00
SkateSpots
fead9ca781 feat(trust): spec-correct NIP-85 consumer + Verified/Unknown/Flagged badge
First slice of TRUST_SCORE.md.

- lib/nip85.ts: parse kind-10040 provider designations, resolve a provider
  with a house fallback, parse kind-30382 assertions (rank/followers/
  reporters/muters/hops; absent hops means no path, absence is not
  distrust), and author-check the provider.
- hooks/useTrustAssertions.ts: batch read from the resolved provider,
  signature-verified via onlyVerified + the author check.
- lib/trustLadder.ts: three buckets and thresholds (flagged = trusted
  reporters >= 2; verified rank >= 2).
- hooks/useTrustBucket.ts: prefers a NIP-85 assertion (verify + flag),
  falls back to the local WoT ladder (verify only).
- TrustBadge.tsx: now renders Verified / Unknown / Flagged with glyph,
  colour and tooltip; Unknown hidden by default, Flagged always shown.
  Reaches the map/tour/event cards, marketplace card, listing detail and
  coach detail through the shared component.

Open follow-ups (in TRUST_SCORE.md): batch the bucket at list level, a
'who reported' breakdown, a provider-choice setting, alerts, confidence
scoring.
2026-10-09 17:07:34 +07:00
SkateSpots
486ffdb000 feat(map): add autocomplete suggestions to the search bar
Typing in the map search pill now opens a suggestions dropdown built
from the items that already match the active filters:
- Spots: title matches; selecting one flies to it and opens its sheet.
- Places: distinct city/region/country tokens split out of each item's
  location; selecting one sets the query to that token (e.g. spain ->
  Spain).

buildSearchSuggestions lives in lib (pure, testable) and
MapSearchSuggestions.tsx is presentational; MapPage owns the list and
keyboard nav (ArrowUp/Down, Enter, Escape). Adds mapPage.searchSuggestPlaces
(en/es only; other locales fall back to English).
2026-10-09 16:31:31 +07:00