Skip to content

Commit 5afe060

Browse files
dfallingclaude
andauthored
Show loading spinner and keep prior pins during viewport fetches (#4)
Two map UX tweaks: - Overlay an ActivityIndicator while we're waiting on the initial position fix so the 1–2s of dead air before the fly-to no longer looks like the app froze. - Accumulate elements across viewport fetches into a Map<id, Element> and render from that, so previously-shown pins stay on the map while a new bounds fetch is in flight. Fine for our small per-user dataset. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent af9718c commit 5afe060

1 file changed

Lines changed: 41 additions & 3 deletions

File tree

src/map/MapScreen.tsx

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ import {
88
useCurrentPosition,
99
type ViewStateChangeEvent,
1010
} from '@maplibre/maplibre-react-native';
11-
import {useCallback, useEffect, useRef, useState} from 'react';
11+
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
1212
import type {NativeSyntheticEvent} from 'react-native';
13-
import {StyleSheet, Text, View} from 'react-native';
13+
import {ActivityIndicator, StyleSheet, Text, View} from 'react-native';
1414
import {
15+
type ElementsQuery,
1516
type ElementsQueryVariables,
1617
useElementsQuery,
1718
} from '../graphql/__generated__/types';
1819

20+
type ElementWithLocation = ElementsQuery['elements'][number];
21+
1922
const MAP_STYLE = 'https://tiles.openfreemap.org/styles/liberty';
2023
const USER_ZOOM = 14;
2124

@@ -67,6 +70,26 @@ export function MapScreen() {
6770
variables: bounds ? {bounds} : undefined,
6871
});
6972

73+
// Accumulate elements across viewport fetches so pins persist while a new
74+
// search is in flight. Fine for our small per-user dataset; revisit if we
75+
// ever need eviction or to reflect server-side deletes.
76+
const [elementsById, setElementsById] = useState<
77+
ReadonlyMap<string, ElementWithLocation>
78+
>(new Map());
79+
useEffect(() => {
80+
if (!data?.elements) return;
81+
setElementsById(prev => {
82+
const next = new Map(prev);
83+
for (const el of data.elements) next.set(el.id, el);
84+
return next;
85+
});
86+
}, [data?.elements]);
87+
88+
const elements = useMemo(
89+
() => Array.from(elementsById.values()),
90+
[elementsById],
91+
);
92+
7093
return (
7194
<View style={styles.container}>
7295
<MapLibreMap
@@ -75,7 +98,7 @@ export function MapScreen() {
7598
onRegionDidChange={onRegionDidChange}>
7699
<Camera ref={cameraRef} initialViewState={{center: [0, 20], zoom: 1}} />
77100
<UserLocation animated accuracy />
78-
{data?.elements.map(el =>
101+
{elements.map(el =>
79102
el.location ? (
80103
<Marker
81104
key={el.id}
@@ -88,6 +111,11 @@ export function MapScreen() {
88111
) : null,
89112
)}
90113
</MapLibreMap>
114+
{!position ? (
115+
<View pointerEvents="none" style={styles.loadingOverlay}>
116+
<ActivityIndicator size="large" color="#1d6fe0" />
117+
</View>
118+
) : null}
91119
</View>
92120
);
93121
}
@@ -114,4 +142,14 @@ const styles = StyleSheet.create({
114142
lineHeight: 22,
115143
textAlign: 'center',
116144
},
145+
loadingOverlay: {
146+
position: 'absolute',
147+
top: 0,
148+
left: 0,
149+
right: 0,
150+
bottom: 0,
151+
alignItems: 'center',
152+
justifyContent: 'center',
153+
backgroundColor: 'rgba(255,255,255,0.6)',
154+
},
117155
});

0 commit comments

Comments
 (0)