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.
This commit is contained in:
SkateSpots 2026-10-26 12:00:21 +07:00
parent 6751642eed
commit f88dfe79ab
3 changed files with 24 additions and 17 deletions

View file

@ -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

View file

@ -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],

View file

@ -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],