fix(card,profile): resolve the card's profile via the app's own relays
The card showed the placeholder (now initials) even when the npub had a photo: useAuthor missed profiles that only live on the community relay and could drop a profile its strict schema rejected. Card pages now use a tolerant useCardProfile() that reads kind 0 via querySkateContent (community + user relays) and JSON.parses leniently. Also: useAuthor now queries the community relay as its own parallel branch (a flaky relay in the group used to reject the whole batch); and '_@domain' NIP-05 no longer renders a lone '@'. Verified by rendering a real profile's card headlessly.
This commit is contained in:
parent
9dc7e7429d
commit
0dd7e9bfa9
5 changed files with 75 additions and 19 deletions
|
|
@ -1,8 +1,9 @@
|
|||
import { type NostrEvent, type NostrMetadata, NSchema as n } from '@nostrify/nostrify';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { SKATE_RELAY } from '@/lib/types';
|
||||
|
||||
// Fallback relays for profile aggregation — global indexers
|
||||
// Fallback relays for profile aggregation — global indexers.
|
||||
const FALLBACK_RELAYS = [
|
||||
'wss://purplepag.es',
|
||||
'wss://relay.nostr.band',
|
||||
|
|
@ -23,12 +24,11 @@ export function useAuthor(pubkey: string | undefined) {
|
|||
const querySignal = AbortSignal.any([signal, AbortSignal.timeout(15000)]);
|
||||
|
||||
try {
|
||||
// Query the main pool AND the global profile aggregators IN PARALLEL,
|
||||
// then keep the NEWEST kind 0 across all of them. Doing both at once
|
||||
// (rather than only falling back when the main pool is empty) means a
|
||||
// followed user's real name resolves even when their profile lives on a
|
||||
// big indexer but not on the app's curated skate relay — the previous
|
||||
// "generic npub name" bug.
|
||||
// Query the main pool, the global aggregators, AND the app's own
|
||||
// community relay — each INDEPENDENTLY. They must not be in one group:
|
||||
// a single flaky relay there would reject the whole group and drop a
|
||||
// profile that only lives on the community relay (the "generated name /
|
||||
// missing avatar" bug).
|
||||
const results = await Promise.allSettled([
|
||||
nostr.query(
|
||||
[{ kinds: [0], authors: [pubkey], limit: 1 }],
|
||||
|
|
@ -41,6 +41,10 @@ export function useAuthor(pubkey: string | undefined) {
|
|||
{ signal: AbortSignal.timeout(8000) },
|
||||
);
|
||||
})(),
|
||||
nostr.relay(SKATE_RELAY).query(
|
||||
[{ kinds: [0], authors: [pubkey], limit: 1 }],
|
||||
{ signal: AbortSignal.timeout(8000) },
|
||||
),
|
||||
]);
|
||||
|
||||
// Collect every kind 0 event we found and pick the most recent one.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
import {
|
||||
RIDER_CARD_KIND,
|
||||
|
|
@ -103,3 +104,47 @@ export function useRiderRoles(pubkey: string | undefined) {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
export interface CardProfile {
|
||||
name?: string;
|
||||
picture?: string;
|
||||
nip05?: string;
|
||||
created_at?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The profile fields a card needs (name/avatar/nip05), fetched from the app's
|
||||
* OWN relays via `querySkateContent` with a tolerant JSON parse. `useAuthor`
|
||||
* misses profiles that only live on the community relay and can drop a profile
|
||||
* its strict schema doesn't like — so the card uses this instead.
|
||||
*/
|
||||
export function useCardProfile(pubkey: string | undefined) {
|
||||
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
||||
|
||||
return useQuery<CardProfile>({
|
||||
queryKey: ['card-profile', pubkey, getQueryKeySuffix()],
|
||||
enabled: !!pubkey,
|
||||
staleTime: 5 * 60_000,
|
||||
queryFn: async (c) => {
|
||||
if (!pubkey) return {};
|
||||
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(9000)]);
|
||||
const events = await querySkateContent(
|
||||
[{ kinds: [0], authors: [pubkey], limit: 1 }],
|
||||
{ signal },
|
||||
);
|
||||
const ev = [...events].sort((a, b) => b.created_at - a.created_at)[0];
|
||||
if (!ev) return {};
|
||||
try {
|
||||
const m = JSON.parse(ev.content) as Record<string, unknown>;
|
||||
return {
|
||||
name: (m.display_name as string) || (m.name as string),
|
||||
picture: m.picture as string,
|
||||
nip05: m.nip05 as string,
|
||||
created_at: ev.created_at,
|
||||
};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,9 +203,13 @@ async function drawRiderFront(ctx: CanvasRenderingContext2D, data: CanvasCardDat
|
|||
y += 30;
|
||||
}
|
||||
if (data.nip05) {
|
||||
ctx.fillStyle = p.muted;
|
||||
ctx.font = 'italic 17px Georgia, serif';
|
||||
ctx.fillText(fitText(ctx, `@${data.nip05.split('@')[0]}`, TEXT_W), TEXT_X, y);
|
||||
const [local, domain] = data.nip05.split('@');
|
||||
const handle = local && local !== '_' ? `@${local}` : domain ? domain : '';
|
||||
if (handle) {
|
||||
ctx.fillStyle = p.muted;
|
||||
ctx.font = 'italic 17px Georgia, serif';
|
||||
ctx.fillText(fitText(ctx, handle, TEXT_W), TEXT_X, y);
|
||||
}
|
||||
}
|
||||
|
||||
// Crew monogram circles on the lower band.
|
||||
|
|
@ -431,10 +435,14 @@ export async function drawCardToCanvas(data: CanvasCardData, side: 'front' | 'ba
|
|||
} else {
|
||||
ctx.fillStyle = p.sidebarBg;
|
||||
ctx.fillRect(0, 0, PORTRAIT_W, CARD_H);
|
||||
// Initials monogram — more personal than a generic skateboard glyph.
|
||||
const initials = (data.name || '?').trim().split(/\s+/).slice(0, 2).map(w => w[0] ?? '').join('').toUpperCase() || '?';
|
||||
ctx.fillStyle = p.accent;
|
||||
ctx.font = '84px system-ui, sans-serif';
|
||||
ctx.font = 'bold 96px system-ui, sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('🛹', PORTRAIT_W / 2, CARD_H / 2 + 26);
|
||||
ctx.globalAlpha = 0.85;
|
||||
ctx.fillText(initials, PORTRAIT_W / 2, CARD_H / 2 + 34);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
const shade = ctx.createLinearGradient(0, CARD_H - 180, 0, CARD_H);
|
||||
|
|
|
|||
|
|
@ -10,9 +10,8 @@ import { nip19 } from 'nostr-tools';
|
|||
import { useSeoMeta } from '@unhead/react';
|
||||
import { ArrowLeft, Sparkles, RefreshCw } from 'lucide-react';
|
||||
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useRiderStats } from '@/hooks/useRiderStats';
|
||||
import { useRiderCard, useRiderBadges, useRiderRoles } from '@/hooks/useRiderCard';
|
||||
import { useRiderCard, useRiderBadges, useRiderRoles, useCardProfile } from '@/hooks/useRiderCard';
|
||||
import { useUserCrews } from '@/hooks/useCrews';
|
||||
import { useUserSkaterProfile } from '@/hooks/useSkaters';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
|
|
@ -38,8 +37,8 @@ export default function PublicCardPage() {
|
|||
const pubkey = decodePubkey(npub);
|
||||
const { user } = useCurrentUser();
|
||||
|
||||
const author = useAuthor(pubkey ?? '');
|
||||
const metadata = author.data?.metadata;
|
||||
const cardProfile = useCardProfile(pubkey ?? undefined);
|
||||
const metadata = cardProfile.data;
|
||||
const displayName = metadata?.name ?? (pubkey ? genUserName(pubkey) : '');
|
||||
const { data: stats } = useRiderStats(pubkey ?? undefined);
|
||||
const { data: savedCard } = useRiderCard(pubkey ?? undefined);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import { Badge } from '@/components/ui/badge';
|
|||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useRiderStats } from '@/hooks/useRiderStats';
|
||||
import { useRiderCard, useRiderRoles } from '@/hooks/useRiderCard';
|
||||
import { useRiderCard, useRiderRoles, useCardProfile } from '@/hooks/useRiderCard';
|
||||
import { useUserCrews } from '@/hooks/useCrews';
|
||||
import { useUserSkaterProfile } from '@/hooks/useSkaters';
|
||||
import { useSkateSpots } from '@/hooks/useSkateSpots';
|
||||
|
|
@ -164,8 +164,8 @@ export default function RiderCardPage() {
|
|||
});
|
||||
|
||||
const { user } = useCurrentUser();
|
||||
const author = useAuthor(user?.pubkey ?? '');
|
||||
const metadata = author.data?.metadata;
|
||||
const cardProfile = useCardProfile(user?.pubkey);
|
||||
const metadata = cardProfile.data;
|
||||
const { data: stats } = useRiderStats(user?.pubkey);
|
||||
const { data: savedCard } = useRiderCard(user?.pubkey);
|
||||
const { data: allSpots } = useSkateSpots(200);
|
||||
|
|
|
|||
Loading…
Reference in a new issue