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.
This commit is contained in:
parent
37cc0d5d68
commit
84c0ba7b3c
4 changed files with 81 additions and 12 deletions
|
|
@ -2125,11 +2125,14 @@ The community relay moved off the hosted relay.tools instance
|
||||||
relay will lose skate content once the relay.tools subscription is cancelled —
|
relay will lose skate content once the relay.tools subscription is cancelled —
|
||||||
cancel only after the Zapstore update has propagated. Historical newsletter text
|
cancel only after the Zapstore update has propagated. Historical newsletter text
|
||||||
still names the old relay (left as-is on purpose).
|
still names the old relay (left as-is on purpose).
|
||||||
- **Stored relay lists are migrated (2026-10-12):** `RETIRED_RELAY_HOSTS` in
|
- **Stored relay lists are reconciled (2026-10-12):** `RETIRED_RELAY_HOSTS` in
|
||||||
`lib/relays.ts` (`goskatespots.nostr1.com`, dead `wotr.relatr.xyz`) are pruned at
|
`lib/relays.ts` (`goskatespots.nostr1.com`, dead `wotr.relatr.xyz`) are pruned at
|
||||||
runtime (`resolveRelays`), on config load (`AppProvider`) and on NIP-65 sync
|
runtime (`resolveRelays`), on config load (`AppProvider`) and on NIP-65 sync
|
||||||
(`NostrSync`), so existing users stop showing/using them without manual edits.
|
(`NostrSync`); and the community relay (`SKATE_RELAY`) is added to the NIP-65 list
|
||||||
Saving the Relays screen republishes the cleaned NIP-65 list.
|
when missing (unless `useSkateRelay` is off). `NostrSync` also **republishes** the
|
||||||
|
corrected kind-10002 once for non-Amber signers (silent) so other clients see the
|
||||||
|
skate scene; Amber users keep it local (no unprompted popup) and republish from
|
||||||
|
Settings → Relays.
|
||||||
- ⚠️ `relay.skate-spots.org` can show a stale NXDOMAIN in a local resolver cache
|
- ⚠️ `relay.skate-spots.org` can show a stale NXDOMAIN in a local resolver cache
|
||||||
right after creation (our dev box did). Verify with
|
right after creation (our dev box did). Verify with
|
||||||
`curl --resolve relay.skate-spots.org:443:185.18.221.2 …` or from another host.
|
`curl --resolve relay.skate-spots.org:443:185.18.221.2 …` or from another host.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { z } from 'zod';
|
||||||
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
||||||
import { safeJson } from '@/lib/safeJson';
|
import { safeJson } from '@/lib/safeJson';
|
||||||
import { AppContext, type AppConfig, type AppContextType, type Theme, type RelayMetadata } from '@/contexts/AppContext';
|
import { AppContext, type AppConfig, type AppContextType, type Theme, type RelayMetadata } from '@/contexts/AppContext';
|
||||||
import { pruneRetiredRelays } from '@/lib/relays';
|
import { pruneRetiredRelays, ensureCommunityRelay } from '@/lib/relays';
|
||||||
|
|
||||||
interface AppProviderProps {
|
interface AppProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
|
@ -85,13 +85,15 @@ export function AppProvider(props: AppProviderProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// One-time cleanup: drop retired/dead relays from an existing user's saved list.
|
// Reconcile the stored list: drop retired/dead relays and make sure the
|
||||||
// Code defaults change over time, but a user's stored relay list is otherwise
|
// community relay is present (unless the user turned it off with the toggle).
|
||||||
// never rewritten — so a retired relay would linger forever.
|
// Code defaults change over time, but a saved list is otherwise never rewritten.
|
||||||
if (!updates.relayMetadata) {
|
if (!updates.relayMetadata) {
|
||||||
const pruned = pruneRetiredRelays(config.relayMetadata);
|
let md = config.relayMetadata;
|
||||||
if (pruned) {
|
md = pruneRetiredRelays(md) ?? md;
|
||||||
updates.relayMetadata = pruned;
|
if (config.useSkateRelay !== false) md = ensureCommunityRelay(md) ?? md;
|
||||||
|
if (md !== config.relayMetadata) {
|
||||||
|
updates.relayMetadata = md;
|
||||||
needsUpdate = true;
|
needsUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||||
import { useAppContext } from '@/hooks/useAppContext';
|
import { useAppContext } from '@/hooks/useAppContext';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { useTranslation } from '@/contexts/language';
|
import { useTranslation } from '@/contexts/language';
|
||||||
import { isRetiredRelay } from '@/lib/relays';
|
import { isRetiredRelay, withCommunityRelay } from '@/lib/relays';
|
||||||
|
import { isInteractiveSigner } from '@/lib/signerCapabilities';
|
||||||
|
import { useNostrPublish } from '@/hooks/useNostrPublish';
|
||||||
|
import { SKATE_RELAY } from '@/lib/types';
|
||||||
|
|
||||||
// Citrine local relay (Android app by greenart7c3)
|
// Citrine local relay (Android app by greenart7c3)
|
||||||
const CITRINE_URL = 'ws://localhost:4869';
|
const CITRINE_URL = 'ws://localhost:4869';
|
||||||
|
|
@ -49,6 +52,8 @@ export function NostrSync() {
|
||||||
const { config, updateConfig } = useAppContext();
|
const { config, updateConfig } = useAppContext();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { mutate: publishEvent } = useNostrPublish();
|
||||||
|
const communityHandled = useRef(false);
|
||||||
|
|
||||||
// Track per-pubkey: null = never tried, false = failed, true = succeeded
|
// Track per-pubkey: null = never tried, false = failed, true = succeeded
|
||||||
const syncState = useRef<Map<string, boolean>>(new Map());
|
const syncState = useRef<Map<string, boolean>>(new Map());
|
||||||
|
|
@ -80,6 +85,46 @@ export function NostrSync() {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [user?.pubkey]);
|
}, [user?.pubkey]);
|
||||||
|
|
||||||
|
// Ensure the community relay is in the user's NIP-65 list. Users with a silent
|
||||||
|
// signer also get the list republished once so OTHER Nostr clients see the skate
|
||||||
|
// scene; Amber users keep it local (they can republish from Settings → Relays).
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user || communityHandled.current) return;
|
||||||
|
communityHandled.current = true;
|
||||||
|
if (config.useSkateRelay === false) return;
|
||||||
|
|
||||||
|
const has = config.relayMetadata.relays.some(
|
||||||
|
(r) => r.url.toLowerCase() === SKATE_RELAY.toLowerCase(),
|
||||||
|
);
|
||||||
|
if (has) return;
|
||||||
|
|
||||||
|
const list = withCommunityRelay(config.relayMetadata.relays);
|
||||||
|
updateConfig((c) => ({
|
||||||
|
...c,
|
||||||
|
relayMetadata: { ...(c.relayMetadata ?? { relays: [], updatedAt: 0 }), relays: list },
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Amber prompts per op — never publish unprompted. And only republish for a
|
||||||
|
// user who has actually published a NIP-65 list before.
|
||||||
|
if (isInteractiveSigner(user, logins)) return;
|
||||||
|
if (config.relayMetadata.updatedAt === 0) return;
|
||||||
|
|
||||||
|
const tags = list
|
||||||
|
.map((r) =>
|
||||||
|
r.read && r.write
|
||||||
|
? ['r', r.url]
|
||||||
|
: r.read
|
||||||
|
? ['r', r.url, 'read']
|
||||||
|
: r.write
|
||||||
|
? ['r', r.url, 'write']
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
.filter((t): t is string[] => t !== null);
|
||||||
|
publishEvent({ kind: 10002, content: '', tags });
|
||||||
|
// Run once per logged-in user.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [user?.pubkey]);
|
||||||
|
|
||||||
async function doSync(pubkey: string, attempt = 1) {
|
async function doSync(pubkey: string, attempt = 1) {
|
||||||
// Always invalidate stale cache first so useAuthor refetches
|
// Always invalidate stale cache first so useAuthor refetches
|
||||||
queryClient.invalidateQueries({ queryKey: ['author', pubkey] });
|
queryClient.invalidateQueries({ queryKey: ['author', pubkey] });
|
||||||
|
|
@ -132,7 +177,7 @@ export function NostrSync() {
|
||||||
if (events.length > 0) {
|
if (events.length > 0) {
|
||||||
const event = events[0];
|
const event = events[0];
|
||||||
if (event.created_at > config.relayMetadata.updatedAt) {
|
if (event.created_at > config.relayMetadata.updatedAt) {
|
||||||
const fetchedRelays = event.tags
|
let fetchedRelays = event.tags
|
||||||
.filter(([name]) => name === 'r')
|
.filter(([name]) => name === 'r')
|
||||||
.map(([, url, marker]) => ({
|
.map(([, url, marker]) => ({
|
||||||
url,
|
url,
|
||||||
|
|
@ -141,6 +186,10 @@ export function NostrSync() {
|
||||||
}))
|
}))
|
||||||
.filter((r) => !isRetiredRelay(r.url));
|
.filter((r) => !isRetiredRelay(r.url));
|
||||||
if (fetchedRelays.length > 0) {
|
if (fetchedRelays.length > 0) {
|
||||||
|
// Never let a fetched list drop the community relay.
|
||||||
|
if (config.useSkateRelay !== false) {
|
||||||
|
fetchedRelays = withCommunityRelay(fetchedRelays);
|
||||||
|
}
|
||||||
updateConfig(c => ({
|
updateConfig(c => ({
|
||||||
...c,
|
...c,
|
||||||
relayMetadata: {
|
relayMetadata: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useAppContext } from '@/hooks/useAppContext';
|
import { useAppContext } from '@/hooks/useAppContext';
|
||||||
import type { RelayMetadata } from '@/contexts/AppContext';
|
import type { RelayMetadata } from '@/contexts/AppContext';
|
||||||
|
import { SKATE_RELAY } from '@/lib/types';
|
||||||
|
|
||||||
/** Host (no scheme) for display, falling back to the raw value. */
|
/** Host (no scheme) for display, falling back to the raw value. */
|
||||||
export function relayHost(url: string): string {
|
export function relayHost(url: string): string {
|
||||||
|
|
@ -64,6 +65,20 @@ export function pruneRetiredRelays(metadata: RelayMetadata): RelayMetadata | nul
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Add the community relay to a NIP-65 relay list when it's missing. */
|
||||||
|
export function withCommunityRelay<T extends { url: string; read: boolean; write: boolean }>(
|
||||||
|
relays: T[],
|
||||||
|
): T[] {
|
||||||
|
if (relays.some((r) => r.url.toLowerCase() === SKATE_RELAY.toLowerCase())) return relays;
|
||||||
|
return [{ url: SKATE_RELAY, read: true, write: true } as T, ...relays];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@link withCommunityRelay} for a RelayMetadata; null when nothing changed. */
|
||||||
|
export function ensureCommunityRelay(metadata: RelayMetadata): RelayMetadata | null {
|
||||||
|
const relays = withCommunityRelay(metadata.relays);
|
||||||
|
return relays === metadata.relays ? null : { ...metadata, relays };
|
||||||
|
}
|
||||||
|
|
||||||
export interface ResolvedRelays {
|
export interface ResolvedRelays {
|
||||||
/** NIP-65 relays with blocked ones removed (read/write flags kept). */
|
/** NIP-65 relays with blocked ones removed (read/write flags kept). */
|
||||||
configured: { url: string; read: boolean; write: boolean }[];
|
configured: { url: string; read: boolean; write: boolean }[];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue