From e1a2b9124105526187bc4849e768c9e9b7e821f8 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sat, 3 Oct 2026 16:23:25 +0700 Subject: [PATCH] feat(map): show the user's own Nostr avatar on the "you are here" pin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live-location marker used the SkateSpots brand logo. It now uses the logged-in user's Nostr profile picture (their npub's logo), falling back to the brand logo when there is none. - SpotMap takes an optional userPicture; createUserLocationIcon layers the avatar over the brand logo (CSS multiple backgrounds) so a broken/expired avatar URL just reveals the logo — no inline JS, CSP-safe. - The icon refreshes when the picture loads (or changes) without moving the pin. - MapPage passes the current user's metadata picture. --- src/components/SpotMap.tsx | 30 +++++++++++++++++++++++------- src/pages/MapPage.tsx | 4 ++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/components/SpotMap.tsx b/src/components/SpotMap.tsx index bda9fc5..04195db 100644 --- a/src/components/SpotMap.tsx +++ b/src/components/SpotMap.tsx @@ -193,11 +193,22 @@ const createShopIcon = () => L.divIcon({ // "You are here" user-location marker — SkateSpots logo in a circular badge // with a pulsing accuracy halo (Google-Maps-style, brand-flavored). -const createUserLocationIcon = () => { +const createUserLocationIcon = (picture?: string | null) => { // BASE_URL resolves correctly for both web ('/') and the Capacitor APK ('./'). // Reuse the small (~24 KB) brand logo for the marker instead of the old // 2.9 MB user-location.png — same artwork, ~99% smaller payload. const logoUrl = `${import.meta.env.BASE_URL}images/logo.png`; + + // Show the user's own Nostr profile picture (their npub's "logo") when set. + // Layered CSS backgrounds mean a broken/expired avatar URL silently reveals + // the brand logo underneath — no inline JS (CSP-safe). + const safeAvatar = picture ? sanitizeUrl(picture) : null; + const avatarCss = safeAvatar + ? encodeURI(safeAvatar).replace(/[()'"]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`) + : ''; + const backgroundImage = avatarCss ? `url('${avatarCss}'), url('${logoUrl}')` : `url('${logoUrl}')`; + const backgroundSize = avatarCss ? 'cover, cover' : 'cover'; + return L.divIcon({ className: 'user-location-marker', html: ` @@ -212,15 +223,15 @@ const createUserLocationIcon = () => { background: rgba(255,0,64,0.25); animation: ssPulse 2s ease-out infinite; "> - +
(null); @@ -1041,12 +1055,14 @@ export function SpotMap({ } if (userMarkerRef.current) { - // Move the existing marker to the new position. + // Move the existing marker to the new position and refresh its icon + // (the profile picture can arrive after the marker was first created). userMarkerRef.current.setLatLng([userLocation.lat, userLocation.lng]); + userMarkerRef.current.setIcon(createUserLocationIcon(userPicture)); } else { // Create it, on top of everything else. userMarkerRef.current = L.marker([userLocation.lat, userLocation.lng], { - icon: createUserLocationIcon(), + icon: createUserLocationIcon(userPicture), zIndexOffset: 1000, interactive: true, keyboard: false, @@ -1054,7 +1070,7 @@ export function SpotMap({ .addTo(map) .bindPopup(`${escapeHtml(t('mapUi.youAreHere'))}`); } - }, [userLocation, t]); + }, [userLocation, userPicture, t]); // Update markers useEffect(() => { diff --git a/src/pages/MapPage.tsx b/src/pages/MapPage.tsx index f1dca5d..91f678b 100644 --- a/src/pages/MapPage.tsx +++ b/src/pages/MapPage.tsx @@ -541,6 +541,9 @@ export default function MapPage() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { user } = useCurrentUser(); + // The user's own profile picture, shown on the "you are here" pin. + const meAuthor = useAuthor(user?.pubkey || ''); + const userPicture = meAuthor.data?.metadata?.picture ?? null; const { data: followingList } = useFollowing(user?.pubkey); const followCount = followingList?.length ?? 0; const { data: spots, isLoading: spotsLoading, refetch: refetchSpots } = useSkateSpots(2000); @@ -1674,6 +1677,7 @@ export default function MapPage() { showBtcMap={showBtcMap} flyTo={flyTo} userLocation={isMobile && userLocation ? { lat: userLocation.lat, lng: userLocation.lon } : null} + userPicture={userPicture} /> {/* Loading indicator — a slim, honest top bar. The map stays