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:
parent
6751642eed
commit
f88dfe79ab
3 changed files with 24 additions and 17 deletions
23
AGENTS.md
23
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
|
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
|
passed while real users crashed. Any new field on an allow-listed query needs
|
||||||
both.
|
both.
|
||||||
- ⚠️ **The map and the stats legitimately differ — do NOT "fix" the map to
|
- ✅ **The map is now CANONICAL for spots + shops (owner decision, 2026-10-26).**
|
||||||
`queryCanonical` (2026-10-01).** The map (`useSkateSpots` →
|
`useSkateSpots` and `useShops` use `queryCanonical` (community relay
|
||||||
`querySkateContent`) reads the community relay **+ the reader's own relays**;
|
`relay.skate-spots.org` ONLY) with a stable `'canonical'` query-key suffix, so
|
||||||
the stats read the community relay only (`queryCanonical`). Forcing canonical
|
the map always matches the community dataset regardless of the viewer's
|
||||||
on the map would hide spots for a user who opted out of the community relay
|
personal relays. This **supersedes** the 2026-10-01 note below (which kept the
|
||||||
(`useSkateRelay=false`) — including **their own**. Two more reasons a "23"
|
map blended so a `useSkateRelay=false` user still saw their own pins). The
|
||||||
cluster ≠ a country row: a cluster bubble is proximity (may span countries),
|
spot **detail** page (`useSkateSpot`) stays blended so shared links to a spot
|
||||||
and the region card only counts locations **with a location tag** while the
|
published only on a user relay still resolve. Remaining map↔stats deltas:
|
||||||
map draws every pin. The number comparable to the map is the hero
|
a cluster bubble is proximity (may span countries), and the region card only
|
||||||
**"Skate Spots"** card (`totalSpots`), not the region rows.
|
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`
|
- **Moderation now applies to the stats too (2026-10-01):** `useGrowthStats`
|
||||||
calls `useAdminModeration()` and drops hidden content inside `dedup()`, so a
|
calls `useAdminModeration()` and drops hidden content inside `dedup()`, so a
|
||||||
spot the community removed no longer inflates `totalSpots`, the regions, or
|
spot the community removed no longer inflates `totalSpots`, the regions, or
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,16 @@ import { SKATE_SHOP_KIND, SKATE_SPOT_KIND, parseSkateShop, parseSkateSpot, type
|
||||||
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
||||||
|
|
||||||
export function useShops(limit = 100) {
|
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({
|
return useQuery({
|
||||||
queryKey: ['shop-directory', 'list', limit, getQueryKeySuffix()],
|
queryKey: ['shop-directory', 'list', limit, 'canonical'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const signal = AbortSignal.timeout(10000);
|
const signal = AbortSignal.timeout(10000);
|
||||||
|
|
||||||
// Query new shop kind (30017)
|
// Query new shop kind (30017)
|
||||||
const newShops = await querySkateContent(
|
const newShops = await queryCanonical(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
kinds: [SKATE_SHOP_KIND],
|
kinds: [SKATE_SHOP_KIND],
|
||||||
|
|
@ -22,7 +23,7 @@ export function useShops(limit = 100) {
|
||||||
{ signal, requireAny: true }
|
{ signal, requireAny: true }
|
||||||
);
|
);
|
||||||
// Query old shops (kind 30818 with 'shop' type) for backward compatibility
|
// Query old shops (kind 30818 with 'shop' type) for backward compatibility
|
||||||
const oldShops = await querySkateContent(
|
const oldShops = await queryCanonical(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
kinds: [SKATE_SPOT_KIND],
|
kinds: [SKATE_SPOT_KIND],
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,14 @@ const QUERY_TIMEOUT = 15000; // 15s — mobile cold-start needs mo
|
||||||
*/
|
*/
|
||||||
export function useSkateSpots(limit = 100, options: { includeHidden?: boolean } = {}) {
|
export function useSkateSpots(limit = 100, options: { includeHidden?: boolean } = {}) {
|
||||||
const { includeHidden = false } = options;
|
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();
|
const hidden = useAdminModeration();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['skate-spots', limit, includeHidden, getQueryKeySuffix()],
|
queryKey: ['skate-spots', limit, includeHidden, 'canonical'],
|
||||||
select: (spots: SkateSpot[]) =>
|
select: (spots: SkateSpot[]) =>
|
||||||
includeHidden ? spots : filterAdminHidden(spots, SKATE_SPOT_KIND, hidden),
|
includeHidden ? spots : filterAdminHidden(spots, SKATE_SPOT_KIND, hidden),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
|
@ -41,7 +44,7 @@ export function useSkateSpots(limit = 100, options: { includeHidden?: boolean }
|
||||||
const signal = AbortSignal.timeout(QUERY_TIMEOUT);
|
const signal = AbortSignal.timeout(QUERY_TIMEOUT);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const events = await querySkateContent(
|
const events = await queryCanonical(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
kinds: [SKATE_SPOT_KIND],
|
kinds: [SKATE_SPOT_KIND],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue