lazy loading maps

This commit is contained in:
muerwre 2019-02-18 18:03:03 +07:00
parent 945878f2df
commit 559d6c8d07
11 changed files with 157 additions and 25 deletions

View file

@ -3,7 +3,7 @@ const { Route, User } = require('../../models');
module.exports = async (req, res) => {
const {
query: {
id, token, title, distance, author, starred,
id, token, title, distance, author, step = 20, shift = 0,
}
} = req;
@ -34,7 +34,7 @@ module.exports = async (req, res) => {
},
'_id title distance owner updated_at is_public',
{
limit: 500,
limit: 9000,
sort: { updated_at: -1 },
}
).populate('owner', '_id');
@ -61,6 +61,12 @@ module.exports = async (req, res) => {
));
}
const limit = list.length;
if (step) {
list = list.slice(parseInt(shift, 10), (parseInt(shift, 10) + parseInt(step, 10)));
}
if (list.length === 0) {
limits = { min: 0, max: 0 };
} else if (limits.max === 0) {
@ -74,6 +80,9 @@ module.exports = async (req, res) => {
res.send({
success: true,
list,
limit,
step: parseInt(step, 10),
shift: parseInt(shift, 10),
...limits,
});
};

View file

@ -141,7 +141,7 @@ const run = async () => {
title: '',
version: 1,
distance: calcPolyDistance(route),
is_public: false,
is_public: true,
};
}));
}));

5
package-lock.json generated
View file

@ -11461,6 +11461,11 @@
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
"dev": true
},
"throttle-debounce": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.1.0.tgz",
"integrity": "sha512-AOvyNahXQuU7NN+VVvOOX+uW6FPaWdAOdRP5HfwYxAfCzXTFKRMoIMk+n+po318+ktcChx+F1Dd91G3YHeMKyg=="
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",

View file

@ -100,6 +100,7 @@
"scrypt": "^6.0.3",
"styled-components": "^3.2.6",
"styled-theming": "^2.2.0",
"throttle-debounce": "^2.1.0",
"tt-react-custom-scrollbars": "^4.2.1-tt2",
"typeface-pt-sans": "0.0.54",
"typesafe-actions": "^3.0.0",

View file

@ -7,7 +7,7 @@ import {
searchSetDistance,
searchSetTitle,
searchSetTab,
setDialogActive,
setDialogActive, mapsLoadMore,
} from '$redux/user/actions';
import { isMobile } from '$utils/window';
import classnames from 'classnames';
@ -21,7 +21,7 @@ import { IRootState } from '$redux/user/reducer';
interface Props extends IRootState {
marks: { [x: number]: string },
routes_sorted: Array<string>,
mapsLoadMore: typeof mapsLoadMore,
searchSetDistance: typeof searchSetDistance,
searchSetTitle: typeof searchSetTitle,
searchSetTab: typeof searchSetTab,
@ -50,6 +50,19 @@ class Component extends React.Component<Props, State> {
pushPath(`/${_id}/${this.props.editing ? 'edit' : ''}`);
};
onScroll = (e: { target: { scrollHeight: number, scrollTop: number, clientHeight: number }}): void => {
const { target: { scrollHeight, scrollTop, clientHeight }} = e;
const delta = scrollHeight - scrollTop - clientHeight;
if (
delta < 300 &&
this.props.routes.list.length < this.props.routes.limit &&
!this.props.routes.loading
) {
this.props.mapsLoadMore();
}
};
render() {
const {
ready,
@ -130,7 +143,10 @@ class Component extends React.Component<Props, State> {
</div>
</div>
<Scroll className="dialog-shader">
<Scroll
className="dialog-shader"
onScroll={this.onScroll}
>
<div className="dialog-maplist">
{
list.map(route => (
@ -148,7 +164,7 @@ class Component extends React.Component<Props, State> {
}
</div>
</Scroll>
<div className={classnames('dialog-maplist-pulse', { active: loading })} />
</div>
);
}
@ -178,6 +194,7 @@ const mapDispatchToProps = dispatch => bindActionCreators({
searchSetTitle,
searchSetTab,
setDialogActive,
mapsLoadMore,
}, dispatch);
export const MapListDialog = connect(mapStateToProps, mapDispatchToProps)(Component);

View file

@ -56,6 +56,7 @@ export const keyPressed = ({ key, target: { tagName } }) => ({ type: ACTIONS.KEY
export const searchSetTitle = title => ({ type: ACTIONS.SEARCH_SET_TITLE, title });
export const searchSetDistance = distance => ({ type: ACTIONS.SEARCH_SET_DISTANCE, distance });
export const searchChangeDistance = distance => ({ type: ACTIONS.SEARCH_CHANGE_DISTANCE, distance });
export const searchSetTab = tab => ({ type: ACTIONS.SEARCH_SET_TAB, tab });
export const searchSetLoading = loading => ({ type: ACTIONS.SEARCH_SET_LOADING, loading });
@ -64,3 +65,6 @@ export const searchPutRoutes = payload => ({ type: ACTIONS.SEARCH_PUT_ROUTES, ..
export const setMarkersShown = markers_shown => ({ type: ACTIONS.SET_MARKERS_SHOWN, markers_shown });
export const getGPXTrack = () => ({ type: ACTIONS.GET_GPX_TRACK });
export const setIsEmpty = is_empty => ({ type: ACTIONS.SET_IS_EMPTY, is_empty });
export const mapsLoadMore = () => ({ type: ACTIONS.MAPS_LOAD_MORE });
export const mapsSetShift = (shift: number) => ({ type: ACTIONS.MAPS_SET_SHIFT, shift });

View file

@ -59,6 +59,7 @@ export const ACTIONS: IActions = {
SEARCH_SET_TITLE: 'SEARCH_SET_TITLE',
SEARCH_SET_DISTANCE: 'SEARCH_SET_DISTANCE',
SEARCH_CHANGE_DISTANCE: 'SEARCH_CHANGE_DISTANCE',
SEARCH_SET_TAB: 'SEARCH_SET_TAB',
SEARCH_PUT_ROUTES: 'SEARCH_PUT_ROUTES',
@ -71,4 +72,7 @@ export const ACTIONS: IActions = {
GET_GPX_TRACK: 'GET_GPX_TRACK',
SET_IS_EMPTY: 'SET_IS_EMPTY',
MAPS_LOAD_MORE: 'MAPS_LOAD_MORE',
MAPS_SET_SHIFT: 'MAPS_SET_SHIFT',
};

View file

@ -57,6 +57,8 @@ interface IRootReducer {
limit: 0,
loading: boolean,
list: Array<IRoute>,
step: number,
shift: number,
filter: {
title: '',
starred: boolean,
@ -237,11 +239,14 @@ const searchSetTab: ActionHandler<typeof ActionCreators.searchSetTab> = (state,
}
});
const searchPutRoutes: ActionHandler<typeof ActionCreators.searchPutRoutes> = (state, { list = [], min, max }) => ({
const searchPutRoutes: ActionHandler<typeof ActionCreators.searchPutRoutes> = (state, { list = [], min, max, limit, step, shift }) => ({
...state,
routes: {
...state.routes,
list,
limit,
step,
shift,
filter: {
...state.routes.filter,
distance: (state.routes.filter.min === state.routes.filter.max)
@ -270,6 +275,13 @@ const setSpeed: ActionHandler<typeof ActionCreators.setSpeed> = (state, { speed
const setMarkersShown: ActionHandler<typeof ActionCreators.setMarkersShown> = (state, { markers_shown = true }) => ({ ...state, markers_shown });
const setIsEmpty: ActionHandler<typeof ActionCreators.setIsEmpty> = (state, { is_empty = true }) => ({ ...state, is_empty });
const mapsSetShift: ActionHandler<typeof ActionCreators.mapsSetShift> = (state, { shift = 0 }) => ({
...state,
routes: {
...state.routes,
shift,
}
});
const HANDLERS = ({
[ACTIONS.SET_USER]: setUser,
@ -302,6 +314,7 @@ const HANDLERS = ({
[ACTIONS.SEARCH_SET_TITLE]: searchSetTitle,
[ACTIONS.SEARCH_SET_DISTANCE]: searchSetDistance,
[ACTIONS.SEARCH_CHANGE_DISTANCE]: searchSetDistance,
[ACTIONS.SEARCH_SET_TAB]: searchSetTab,
[ACTIONS.SEARCH_PUT_ROUTES]: searchPutRoutes,
[ACTIONS.SEARCH_SET_LOADING]: searchSetLoading,
@ -310,6 +323,8 @@ const HANDLERS = ({
[ACTIONS.SET_MARKERS_SHOWN]: setMarkersShown,
[ACTIONS.SET_IS_EMPTY]: setIsEmpty,
[ACTIONS.MAPS_SET_SHIFT]: mapsSetShift,
});
export const INITIAL_STATE: IRootReducer = {
@ -354,6 +369,8 @@ export const INITIAL_STATE: IRootReducer = {
limit: 0,
loading: false, // <-- maybe delete this
list: [],
step: 20,
shift: 0,
filter: {
title: '',
starred: false,

View file

@ -9,15 +9,32 @@ import {
postMap
} from '$utils/api';
import {
hideRenderer, searchPutRoutes, searchSetDistance, searchSetLoading,
setActiveSticker, setAddress,
setChanged, setDialogActive,
hideRenderer,
searchPutRoutes,
searchSetDistance,
searchSetLoading,
setActiveSticker,
setAddress,
setChanged,
setDialogActive,
setEditing,
setMode, setReady, setRenderer,
setMode,
setReady,
setRenderer,
setSaveError,
setSaveOverwrite, setSaveSuccess, setTitle,
setSaveOverwrite,
setSaveSuccess,
setTitle,
searchSetTab,
setUser, setDialog, setPublic, setAddressOrigin, setProvider, changeProvider, openMapDialog, setSaveLoading,
setUser,
setDialog,
setPublic,
setAddressOrigin,
setProvider,
changeProvider,
openMapDialog,
setSaveLoading,
mapsSetShift, searchChangeDistance,
} from '$redux/user/actions';
import { getUrlData, parseQuery, pushLoaderState, pushNetworkInitError, pushPath, replacePath } from '$utils/history';
import { editor } from '$modules/Editor';
@ -456,28 +473,36 @@ function* keyPressedSaga({ key, target }: ReturnType<typeof ActionCreators.keyPr
}
}
function* searchSetSagaWorker() {
function* searchGetRoutes() {
const { id, token } = yield select(getUser);
const { routes: { filter, filter: { title, distance, tab } } } = yield select(getState);
const { routes: { step, shift, filter: { title, distance, tab } } } = yield select(getState);
const { list, min, max } = yield call(getRouteList, {
return yield call(getRouteList, {
id,
token,
title,
distance,
step,
shift,
author: tab === 'mine' ? id : '',
starred: tab === 'starred',
});
}
yield put(searchPutRoutes({ list, min, max }));
function* searchSetSagaWorker() {
const { routes: { filter } } = yield select(getState);
const { list, min, max, limit, shift, step } = yield call(searchGetRoutes);
yield put(searchPutRoutes({ list, min, max, limit, shift, step }));
// change distange range if needed and load additional data
if (
(filter.min > min && filter.distance[0] <= filter.min) ||
(filter.max < max && filter.distance[1] >= filter.max)
) {
yield put(searchSetDistance([
yield put(searchChangeDistance([
(filter.min > min && filter.distance[0] <= filter.min)
? min
: filter.distance[0],
@ -492,6 +517,7 @@ function* searchSetSagaWorker() {
function* searchSetSaga() {
yield put(searchSetLoading(true));
yield put(mapsSetShift(0));
yield delay(500);
yield call(searchSetSagaWorker);
}
@ -514,8 +540,8 @@ function* openMapDialogSaga({ tab }: ReturnType<typeof ActionCreators.openMapDia
}
function* searchSetTabSaga() {
yield put(searchSetDistance([0, 10000]));
yield put(searchPutRoutes({ list: [], min: 0, max: 10000 }));
yield put(searchChangeDistance([0, 10000]));
yield put(searchPutRoutes({ list: [], min: 0, max: 10000, step: 20, shift: 0 }));
yield call(searchSetSaga);
}
@ -572,6 +598,36 @@ function* getGPXTrackSaga(): SagaIterator {
return downloadGPXTrack({ track, title });
}
function* mapsLoadMoreSaga() {
const { routes: { limit, list, shift, step, loading, filter } } = yield select(getState);
if (loading || list.length >= limit || limit === 0) return;
yield delay(500);
yield put(searchSetLoading(true));
yield put(mapsSetShift(shift + step));
const result = yield call(searchGetRoutes);
if (
(filter.min > result.min && filter.distance[0] <= filter.min) ||
(filter.max < result.max && filter.distance[1] >= filter.max)
) {
yield put(searchChangeDistance([
(filter.min > result.min && filter.distance[0] <= filter.min)
? result.min
: filter.distance[0],
(filter.max < result.max && filter.distance[1] >= filter.max)
? result.max
: filter.distance[1],
]));
}
yield put(searchPutRoutes({ ...result, list: [...list, ...result.list] }));
yield put(searchSetLoading(false));
}
export function* userSaga() {
yield takeLatest(REHYDRATE, authCheckSaga);
yield takeEvery(ACTIONS.SET_MODE, setModeSaga);
@ -614,5 +670,6 @@ export function* userSaga() {
yield takeLatest(ACTIONS.SEARCH_SET_TAB, searchSetTabSaga);
yield takeLatest(ACTIONS.SET_USER, setUserSaga);
yield takeLatest(ACTIONS.GET_GPX_TRACK, getGPXTrackSaga)
yield takeLatest(ACTIONS.GET_GPX_TRACK, getGPXTrackSaga);
yield takeLatest(ACTIONS.MAPS_LOAD_MORE, mapsLoadMoreSaga);
}

View file

@ -126,6 +126,24 @@
100% { transform: rotate(360deg); }
}
.dialog-maplist-pulse {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
z-index: 10;
background: linear-gradient(fade(@red_secondary, 0%), @red_secondary);
height: 48px;
pointer-events: none;
transition: opacity 250ms;
opacity: 0;
&.active {
opacity: 1;
animation: pulse 500ms infinite alternate;
}
}
.dialog-maplist-loader {
display: flex;
margin-bottom: 10px;

View file

@ -40,10 +40,10 @@ export const checkIframeToken = ({ viewer_id, auth_key }) => axios.get(API.IFRAM
}).then(result => (result && result.data && result.data.success && result.data.user)).catch(() => (false));
export const getRouteList = ({
title, distance, author, starred, id, token,
title, distance, author, starred, id, token, step, shift,
}) => axios.get(API.GET_ROUTE_LIST, {
params: {
title, distance, author, starred, id, token,
title, distance, author, starred, id, token, step, shift
}
}).then(result => (result && result.data && result.data.success && result.data))
.catch(() => ({ list: [], min: 0, max: 0 }));
.catch(() => ({ list: [], min: 0, max: 0, limit: 0, step: 20, shift: 20 }));