feat(map): show the user's own Nostr avatar on the "you are here" pin
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.
This commit is contained in:
parent
7aeda11041
commit
e1a2b91241
2 changed files with 27 additions and 7 deletions
|
|
@ -193,11 +193,22 @@ const createShopIcon = () => L.divIcon({
|
||||||
|
|
||||||
// "You are here" user-location marker — SkateSpots logo in a circular badge
|
// "You are here" user-location marker — SkateSpots logo in a circular badge
|
||||||
// with a pulsing accuracy halo (Google-Maps-style, brand-flavored).
|
// 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 ('./').
|
// 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
|
// 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.
|
// 2.9 MB user-location.png — same artwork, ~99% smaller payload.
|
||||||
const logoUrl = `${import.meta.env.BASE_URL}images/logo.png`;
|
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({
|
return L.divIcon({
|
||||||
className: 'user-location-marker',
|
className: 'user-location-marker',
|
||||||
html: `
|
html: `
|
||||||
|
|
@ -212,15 +223,15 @@ const createUserLocationIcon = () => {
|
||||||
background: rgba(255,0,64,0.25);
|
background: rgba(255,0,64,0.25);
|
||||||
animation: ssPulse 2s ease-out infinite;
|
animation: ssPulse 2s ease-out infinite;
|
||||||
"></span>
|
"></span>
|
||||||
<!-- logo badge -->
|
<!-- avatar / logo badge -->
|
||||||
<div style="
|
<div style="
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%; top: 50%;
|
left: 50%; top: 50%;
|
||||||
width: 40px; height: 40px;
|
width: 40px; height: 40px;
|
||||||
margin: -20px 0 0 -20px;
|
margin: -20px 0 0 -20px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-image: url('${logoUrl}');
|
background-image: ${backgroundImage};
|
||||||
background-size: cover;
|
background-size: ${backgroundSize};
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border: 3px solid #ff0040;
|
border: 3px solid #ff0040;
|
||||||
|
|
@ -527,6 +538,8 @@ interface SpotMapProps {
|
||||||
flyTo?: { lat: number; lng: number; zoom?: number } | null;
|
flyTo?: { lat: number; lng: number; zoom?: number } | null;
|
||||||
/** The current user's location — renders a "you are here" pin when set. */
|
/** The current user's location — renders a "you are here" pin when set. */
|
||||||
userLocation?: { lat: number; lng: number } | null;
|
userLocation?: { lat: number; lng: number } | null;
|
||||||
|
/** The current user's Nostr profile picture, used on the "you are here" pin. */
|
||||||
|
userPicture?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPopupContent(
|
function createPopupContent(
|
||||||
|
|
@ -793,6 +806,7 @@ export function SpotMap({
|
||||||
showBtcMap = false,
|
showBtcMap = false,
|
||||||
flyTo = null,
|
flyTo = null,
|
||||||
userLocation = null,
|
userLocation = null,
|
||||||
|
userPicture = null,
|
||||||
}: SpotMapProps) {
|
}: SpotMapProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mapRef = useRef<HTMLDivElement>(null);
|
const mapRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -1041,12 +1055,14 @@ export function SpotMap({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userMarkerRef.current) {
|
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.setLatLng([userLocation.lat, userLocation.lng]);
|
||||||
|
userMarkerRef.current.setIcon(createUserLocationIcon(userPicture));
|
||||||
} else {
|
} else {
|
||||||
// Create it, on top of everything else.
|
// Create it, on top of everything else.
|
||||||
userMarkerRef.current = L.marker([userLocation.lat, userLocation.lng], {
|
userMarkerRef.current = L.marker([userLocation.lat, userLocation.lng], {
|
||||||
icon: createUserLocationIcon(),
|
icon: createUserLocationIcon(userPicture),
|
||||||
zIndexOffset: 1000,
|
zIndexOffset: 1000,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
keyboard: false,
|
keyboard: false,
|
||||||
|
|
@ -1054,7 +1070,7 @@ export function SpotMap({
|
||||||
.addTo(map)
|
.addTo(map)
|
||||||
.bindPopup(`<b>${escapeHtml(t('mapUi.youAreHere'))}</b>`);
|
.bindPopup(`<b>${escapeHtml(t('mapUi.youAreHere'))}</b>`);
|
||||||
}
|
}
|
||||||
}, [userLocation, t]);
|
}, [userLocation, userPicture, t]);
|
||||||
|
|
||||||
// Update markers
|
// Update markers
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -541,6 +541,9 @@ export default function MapPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { user } = useCurrentUser();
|
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 { data: followingList } = useFollowing(user?.pubkey);
|
||||||
const followCount = followingList?.length ?? 0;
|
const followCount = followingList?.length ?? 0;
|
||||||
const { data: spots, isLoading: spotsLoading, refetch: refetchSpots } = useSkateSpots(2000);
|
const { data: spots, isLoading: spotsLoading, refetch: refetchSpots } = useSkateSpots(2000);
|
||||||
|
|
@ -1674,6 +1677,7 @@ export default function MapPage() {
|
||||||
showBtcMap={showBtcMap}
|
showBtcMap={showBtcMap}
|
||||||
flyTo={flyTo}
|
flyTo={flyTo}
|
||||||
userLocation={isMobile && userLocation ? { lat: userLocation.lat, lng: userLocation.lon } : null}
|
userLocation={isMobile && userLocation ? { lat: userLocation.lat, lng: userLocation.lon } : null}
|
||||||
|
userPicture={userPicture}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
{/* Loading indicator — a slim, honest top bar. The map stays
|
{/* Loading indicator — a slim, honest top bar. The map stays
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue