From f88dfe79ab091e9bbd6b5ebd5e8fa5fa2707f8a4 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Mon, 26 Oct 2026 12:00:21 +0700 Subject: [PATCH] feat(map): spots and shops come only from the community relay Owner decision: the map's spots + shops are canonical (relay.skate-spots.org only), not blended with the viewer's personal relays. useSkateSpots and useShops now use queryCanonical with a stable 'canonical' query-key suffix. The spot detail page (useSkateSpot) stays blended so shared links to a spot published only on a user relay still resolve. Verified with a headless map screenshot showing canonical clusters. --- AGENTS.md | 23 +++++++++++++---------- src/hooks/useShops.ts | 9 +++++---- src/hooks/useSkateSpots.ts | 9 ++++++--- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7aaec5d..1cad704 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1696,16 +1696,19 @@ session with **no other memory of this project**. So: cannot catch this** — it boots a clean profile with no persisted cache, so it passed while real users crashed. Any new field on an allow-listed query needs both. - - ⚠️ **The map and the stats legitimately differ — do NOT "fix" the map to - `queryCanonical` (2026-10-01).** The map (`useSkateSpots` → - `querySkateContent`) reads the community relay **+ the reader's own relays**; - the stats read the community relay only (`queryCanonical`). Forcing canonical - on the map would hide spots for a user who opted out of the community relay - (`useSkateRelay=false`) — including **their own**. Two more reasons a "23" - cluster ≠ a country row: a cluster bubble is proximity (may span countries), - and the region card only counts locations **with a location tag** while the - map draws every pin. The number comparable to the map is the hero - **"Skate Spots"** card (`totalSpots`), not the region rows. + - ✅ **The map is now CANONICAL for spots + shops (owner decision, 2026-10-26).** + `useSkateSpots` and `useShops` use `queryCanonical` (community relay + `relay.skate-spots.org` ONLY) with a stable `'canonical'` query-key suffix, so + the map always matches the community dataset regardless of the viewer's + personal relays. This **supersedes** the 2026-10-01 note below (which kept the + map blended so a `useSkateRelay=false` user still saw their own pins). The + spot **detail** page (`useSkateSpot`) stays blended so shared links to a spot + published only on a user relay still resolve. Remaining map↔stats deltas: + a cluster bubble is proximity (may span countries), and the region card only + counts locations **with a location tag** while the map draws every pin — the + number comparable to the map is the hero **"Skate Spots"** card. + - ~~The map and the stats legitimately differ — do NOT "fix" the map to + `queryCanonical` (2026-10-01).~~ (superseded above) - **Moderation now applies to the stats too (2026-10-01):** `useGrowthStats` calls `useAdminModeration()` and drops hidden content inside `dedup()`, so a spot the community removed no longer inflates `totalSpots`, the regions, or diff --git a/src/hooks/useShops.ts b/src/hooks/useShops.ts index 8ed09b6..32ff385 100644 --- a/src/hooks/useShops.ts +++ b/src/hooks/useShops.ts @@ -3,15 +3,16 @@ import { SKATE_SHOP_KIND, SKATE_SPOT_KIND, parseSkateShop, parseSkateSpot, type import { useSkateRelays } from '@/hooks/useSkateRelays'; export function useShops(limit = 100) { - const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); + // Map/shop directory is canonical: shops come from the community relay only. + const { queryCanonical } = useSkateRelays(); return useQuery({ - queryKey: ['shop-directory', 'list', limit, getQueryKeySuffix()], + queryKey: ['shop-directory', 'list', limit, 'canonical'], queryFn: async () => { const signal = AbortSignal.timeout(10000); // Query new shop kind (30017) - const newShops = await querySkateContent( + const newShops = await queryCanonical( [ { kinds: [SKATE_SHOP_KIND], @@ -22,7 +23,7 @@ export function useShops(limit = 100) { { signal, requireAny: true } ); // Query old shops (kind 30818 with 'shop' type) for backward compatibility - const oldShops = await querySkateContent( + const oldShops = await queryCanonical( [ { kinds: [SKATE_SPOT_KIND], diff --git a/src/hooks/useSkateSpots.ts b/src/hooks/useSkateSpots.ts index d4e2e16..7130bf7 100644 --- a/src/hooks/useSkateSpots.ts +++ b/src/hooks/useSkateSpots.ts @@ -20,11 +20,14 @@ const QUERY_TIMEOUT = 15000; // 15s — mobile cold-start needs mo */ export function useSkateSpots(limit = 100, options: { includeHidden?: boolean } = {}) { const { includeHidden = false } = options; - const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); + // The map is CANONICAL: spots come from the community relay only, so what is + // shown always matches the community dataset regardless of the viewer's + // personal relays. (Details/profile pages stay blended via useSkateSpot.) + const { queryCanonical } = useSkateRelays(); const hidden = useAdminModeration(); return useQuery({ - queryKey: ['skate-spots', limit, includeHidden, getQueryKeySuffix()], + queryKey: ['skate-spots', limit, includeHidden, 'canonical'], select: (spots: SkateSpot[]) => includeHidden ? spots : filterAdminHidden(spots, SKATE_SPOT_KIND, hidden), queryFn: async () => { @@ -41,7 +44,7 @@ export function useSkateSpots(limit = 100, options: { includeHidden?: boolean } const signal = AbortSignal.timeout(QUERY_TIMEOUT); try { - const events = await querySkateContent( + const events = await queryCanonical( [ { kinds: [SKATE_SPOT_KIND],