diff --git a/src/actions/index.js b/src/actions/index.js index 2795b6f1..779baf76 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -10,6 +10,7 @@ import { getSegmentFetchRange, hasRoutesData } from '../timeline/segments'; import { getDeviceFromState, deviceVersionAtLeast } from '../utils'; let routesRequest = null; +let routesRequestPromise = null; export function checkRoutesData() { return (dispatch, getState) => { @@ -22,7 +23,8 @@ export function checkRoutesData() { return; } if (routesRequest && routesRequest.dongleId === state.dongleId) { - return; + // there is already an pending request + return routesRequestPromise; } console.debug('We need to update the segment metadata...'); const { dongleId } = state; @@ -33,7 +35,7 @@ export function checkRoutesData() { dongleId, }; - routesRequest.req.then((routesData) => { + routesRequestPromise = routesRequest.req.then((routesData) => { state = getState(); const currentRange = getSegmentFetchRange(state); if (currentRange.start !== fetchRange.start @@ -83,11 +85,15 @@ export function checkRoutesData() { }); routesRequest = null; + + return routes }).catch((err) => { console.error('Failure fetching routes metadata', err); Sentry.captureException(err, { fingerprint: 'timeline_fetch_routes' }); routesRequest = null; }); + + return routesRequestPromise }; } @@ -389,3 +395,66 @@ export function updateRoute(fullname, route) { route, }; } + +const ONE_DAY = 1000 * 60 * 60 * 24 +const ONE_WEEK = ONE_DAY * 7 +const TWO_WEEK = ONE_WEEK * 2 +const ONE_MONTH = ONE_WEEK * 4 +const TWO_MONTH = ONE_MONTH * 2 +const SIX_MONTH = ONE_MONTH * 6 +const ONE_YEAR = SIX_MONTH * 2 +const lookBackIntervalArray = [ + ONE_DAY, ONE_DAY * 2, ONE_DAY * 3, ONE_DAY * 4, ONE_DAY * 5, + ONE_WEEK, + TWO_WEEK, + ONE_MONTH, + TWO_MONTH, + SIX_MONTH, + ONE_YEAR, +] +let checkRoutesDataWithLookBackInProgress = false + +// check routes and keep extending the start time to 10 days earlier +// until we have more routes +export function checkRoutesDataWithLookBack() { + if (checkRoutesDataWithLookBackInProgress) { + return + } + + return async (dispatch, getState) => { + checkRoutesDataWithLookBackInProgress = true + const state = getState() + const routes = state.routes; + let {start, end} = state.filter + let counter = 0 + + // eslint-disable-next-line no-constant-condition + while (true) { + console.info('fetch with window: ',new Date(start).toDateString()) + + if (counter >= lookBackIntervalArray.length) { + break + } + + try { + dispatch(selectTimeFilter(start, end)); + // eslint-disable-next-line no-await-in-loop + const data = await dispatch(checkRoutesData()); + if (data && ( + !routes && data.length > 1 || + routes && data.length > routes.length) + ) { + checkRoutesDataWithLookBackInProgress = false + break; + } else { + start = start - lookBackIntervalArray[counter]; + counter = counter + 1 + } + } catch (error) { + checkRoutesDataWithLookBackInProgress = false + console.debug('Fetching data failed: ', error); + break; + } + } + }; +} diff --git a/src/components/AppHeader/TimeFilter.jsx b/src/components/AppHeader/TimeFilter.jsx index b9a0bc74..3094caeb 100644 --- a/src/components/AppHeader/TimeFilter.jsx +++ b/src/components/AppHeader/TimeFilter.jsx @@ -6,7 +6,7 @@ import dayjs from 'dayjs'; import { Button, Divider, FormControl, MenuItem, Modal, Paper, Select, Typography, withStyles } from '@material-ui/core'; import Colors from '../../colors'; -import { selectTimeFilter } from '../../actions'; +import { selectTimeFilter, checkRoutesDataWithLookBack } from '../../actions'; import { getDefaultFilter } from '../../initialState'; import VisibilityHandler from '../VisibilityHandler'; @@ -140,6 +140,8 @@ class TimeSelect extends Component { return '1-week'; } if (timeRange === 1000 * 60 * 60 * 24) { return '24-hours'; + } if (timeRange % (1000 * 60 * 60 * 24 * 10) === 0) { + return 'last' } } @@ -167,7 +169,7 @@ class TimeSelect extends Component { onVisible() { const filter = getDefaultFilter(); - this.props.dispatch(selectTimeFilter(filter.start, filter.end)); + this.props.dispatch(checkRoutesDataWithLookBack(filter.start, filter.end)); } render() { @@ -179,7 +181,7 @@ class TimeSelect extends Component { return ( <> - + ({ drivesTable: { @@ -22,22 +23,32 @@ const styles = () => ({ }); const DriveList = (props) => { - const { dispatch, classes, device, routes } = props; - + const { dispatch, classes, device, routes, lastRoutes } = props; + let emptyContent; let content; if (!routes || routes.length === 0) { - content = ; - } else { + emptyContent = ; + } + + // we clean up routes during data fetching, fallback to using lastRoutes to display current data + const displayRoutes = routes || lastRoutes + if (displayRoutes && displayRoutes.length){ // sort routes by start_time_utc_millis with the latest drive first // Workaround upstream sorting issue for now // possibly from https://github.com/commaai/connect/issues/451 - routes.sort((a, b) => b.start_time_utc_millis - a.start_time_utc_millis); + displayRoutes.sort((a, b) => b.start_time_utc_millis - a.start_time_utc_millis); + const routesSize = displayRoutes.length content = (
- {routes.map((drive) => ( - - ))} + {displayRoutes.map((drive, index) => { + // when the second to last item is in view, we fetch the next routes + return (index === routesSize - 2 ? + dispatch(checkRoutesDataWithLookBack())}> + + : + ) + })}
); } @@ -46,12 +57,14 @@ const DriveList = (props) => {
dispatch(checkRoutesData())} minInterval={60} /> {content} + {emptyContent}
); }; const stateToProps = Obstruction({ routes: 'routes', + lastRoutes : 'lastRoutes', device: 'device', }); diff --git a/src/components/ScrollIntoView/index.jsx b/src/components/ScrollIntoView/index.jsx new file mode 100644 index 00000000..293a494a --- /dev/null +++ b/src/components/ScrollIntoView/index.jsx @@ -0,0 +1,39 @@ +import React, { useEffect, useRef } from 'react'; + +const ScrollIntoView = ({ onInView, children, key }) => { + const elementRef = useRef(null); + + useEffect(() => { + const options = { + root: null, // relative to the viewport + rootMargin: '0px', + threshold: 0.1 // 10% of the target's visibility + }; + + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + onInView(); + } + }); + }, options); + + if (elementRef.current) { + observer.observe(elementRef.current); + } + + return () => { + if (observer && elementRef.current) { + observer.unobserve(elementRef.current); + } + }; + }, [onInView]); + + return ( +
+ {children} +
+ ); +}; + +export default ScrollIntoView; diff --git a/src/initialState.js b/src/initialState.js index caefd0c0..f104b79b 100644 --- a/src/initialState.js +++ b/src/initialState.js @@ -5,7 +5,7 @@ export function getDefaultFilter() { d.setHours(d.getHours() + 1, 0, 0, 0); return { - start: (new Date(d.getTime() - 1000 * 60 * 60 * 24 * 14)).getTime(), + start: (new Date(d.getTime() - 1000 * 60 * 60 * 24 * 10)).getTime(), end: d.getTime(), }; } @@ -39,6 +39,7 @@ export default { end: null, }, currentRoute: null, + lastRoutes: null, profile: null, devices: null, diff --git a/src/reducers/globalState.js b/src/reducers/globalState.js index fc0fdb39..6a0d5ae3 100644 --- a/src/reducers/globalState.js +++ b/src/reducers/globalState.js @@ -319,6 +319,7 @@ export default function reducer(_state, action) { .reduce((obj, id) => { obj[id] = state.filesUploading[id]; return obj; }, {}); break; case Types.ACTION_ROUTES_METADATA: + state.lastRoutes = action.routes state.routes = action.routes; state.routesMeta = { dongleId: action.dongleId,