diff --git a/www/js/Main.tsx b/www/js/Main.tsx index 6361e6828..f38b80018 100644 --- a/www/js/Main.tsx +++ b/www/js/Main.tsx @@ -1,7 +1,7 @@ /* Once onboarding is done, this is the main app content. Includes the bottom navigation bar and each of the tabs. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { useContext, useMemo, useState } from 'react'; import { BottomNavigation, useTheme } from 'react-native-paper'; import { AppContext } from './App'; @@ -55,6 +55,12 @@ const Main = () => { return showMetrics ? defaultRoutes(t) : defaultRoutes(t).filter((r) => r.key != 'metrics'); }, [appConfig, t]); + useEffect(() => { + const { setShouldUpdateTimeline } = timelineContext; + // update TimelineScrollList component only when the active tab is 'label' to fix leaflet map issue + setShouldUpdateTimeline(!index); + }, [index]); + return ( void; loadSpecificWeek: (d: string) => void; refreshTimeline: () => void; + shouldUpdateTimeline: Boolean; + setShouldUpdateTimeline: React.Dispatch>; }; export const useTimelineContext = (): ContextProps => { @@ -69,6 +70,9 @@ export const useTimelineContext = (): ContextProps => { const [timelineLabelMap, setTimelineLabelMap] = useState(null); const [timelineNotesMap, setTimelineNotesMap] = useState(null); const [refreshTime, setRefreshTime] = useState(null); + // Leaflet map encounters an error when prerendered, so we need to render the TimelineScrollList component when the active tab is 'label' + // 'shouldUpdateTimeline' gets updated based on the current tab index, and we can use it to determine whether to render the timeline or not + const [shouldUpdateTimeline, setShouldUpdateTimeline] = useState(true); // initialization, once the appConfig is loaded useEffect(() => { @@ -135,10 +139,6 @@ export const useTimelineContext = (): ContextProps => { ); setTimelineLabelMap(newTimelineLabelMap); setTimelineNotesMap(newTimelineNotesMap); - publish('applyLabelTabFilters', { - timelineMap, - timelineLabelMap: newTimelineLabelMap, - }); setTimelineIsLoading(false); }, [timelineMap]); @@ -316,14 +316,6 @@ export const useTimelineContext = (): ContextProps => { }, }; setTimelineLabelMap(newTimelineLabelMap); - setTimeout( - () => - publish('applyLabelTabFilters', { - timelineMap, - timelineLabelMap: newTimelineLabelMap, - }), - 30000, - ); // wait 30s before reapplying filters } else if (inputType == 'note') { const notesForEntry = timelineNotesMap?.[oid] || []; const newAddition = { data: userInput, metadata: { write_ts: nowTs } }; @@ -356,6 +348,8 @@ export const useTimelineContext = (): ContextProps => { notesFor, confirmedModeFor, addUserInputToEntry, + shouldUpdateTimeline, + setShouldUpdateTimeline, }; }; diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index f619fbf4e..ef8507559 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -15,10 +15,9 @@ import { fillLocationNamesOfTrip } from './addressNamesHelper'; import { logDebug } from '../plugin/logger'; import { configuredFilters as multilabelConfiguredFilters } from '../survey/multilabel/infinite_scroll_filters'; import { configuredFilters as enketoConfiguredFilters } from '../survey/enketo/infinite_scroll_filters'; -import { TimelineEntry } from '../types/diaryTypes'; -import TimelineContext, { LabelTabFilter, TimelineLabelMap } from '../TimelineContext'; +import { TimelineEntry, isTrip } from '../types/diaryTypes'; +import TimelineContext, { LabelTabFilter } from '../TimelineContext'; import { AppContext } from '../App'; -import { subscribe } from '../customEventHandler'; type LabelContextProps = { displayedEntries: TimelineEntry[] | null; @@ -29,8 +28,9 @@ export const LabelTabContext = createContext({} as LabelConte const LabelTab = () => { const { appConfig } = useContext(AppContext); - const { pipelineRange, timelineMap } = useContext(TimelineContext); + const { pipelineRange, timelineMap, timelineLabelMap } = useContext(TimelineContext); + const [filterRefreshTs, setFilterRefreshTs] = useState(0); // used to force a refresh of the filters const [filterInputs, setFilterInputs] = useState([]); const [displayedEntries, setDisplayedEntries] = useState(null); @@ -43,46 +43,48 @@ const LabelTab = () => { appConfig.survey_info?.['trip-labels'] == 'ENKETO' ? enketoConfiguredFilters : multilabelConfiguredFilters; - const allFalseFilters = tripFilters.map((f, i) => ({ + const filtersWithState = tripFilters.map((f, i) => ({ ...f, state: i == 0 ? true : false, // only the first filter will have state true on init })); - setFilterInputs(allFalseFilters); + setFilterInputs(filtersWithState); } - - subscribe('applyLabelTabFilters', (e) => { - logDebug('applyLabelTabFilters event received, calling applyFilters'); - applyFilters(e.detail.timelineMap, e.detail.timelineLabelMap || {}); - }); }, [appConfig]); useEffect(() => { if (!timelineMap) return; - const tripsRead = Object.values(timelineMap || {}); - tripsRead - .slice() - .reverse() - .forEach((trip) => fillLocationNamesOfTrip(trip)); + const timelineEntries = Array.from(timelineMap.values()); + if (!timelineEntries?.length) return; + timelineEntries.reverse().forEach((entry) => { + if (isTrip(entry)) fillLocationNamesOfTrip(entry); + }); }, [timelineMap]); - function applyFilters(timelineMap, labelMap: TimelineLabelMap) { + useEffect(() => { + if (!timelineMap || !timelineLabelMap || !filterInputs.length) return; + logDebug('Applying filters'); const allEntries: TimelineEntry[] = Array.from(timelineMap.values()); const activeFilter = filterInputs?.find((f) => f.state == true); let entriesToDisplay = allEntries; if (activeFilter) { - const cutoffTs = new Date().getTime() / 1000 - 30; // 30s ago, as epoch seconds + const nowTs = new Date().getTime() / 1000; const entriesAfterFilter = allEntries.filter((e) => { // if the entry has a recently recorded user input, it is immune to filtering - const labels = labelMap[e._id.$oid]; - for (let labelValue of Object.values(labels || [])) { - logDebug(`LabelTab filtering: labelValue = ${JSON.stringify(labelValue)}`); - if (labelValue?.metadata?.write_ts || 0 > cutoffTs) { - logDebug('LabelTab filtering: entry has recent user input, keeping'); - return true; - } + const labels = timelineLabelMap[e._id.$oid]; + const mostRecentInputTs = Object.values(labels || []).reduce((acc, label) => { + if (label?.metadata?.write_ts && label.metadata.write_ts > acc) + return label.metadata.write_ts; + return acc; + }, 0); + const entryImmuneUntil = mostRecentInputTs + 30; // 30s after the most recent user input + if (entryImmuneUntil > nowTs) { + logDebug(`LabelTab filtering: entry still immune, skipping. + Re-applying filters at ${entryImmuneUntil}`); + setTimeout(() => setFilterRefreshTs(entryImmuneUntil), (entryImmuneUntil - nowTs) * 1000); + return true; } // otherwise, just apply the filter - return activeFilter?.filter(e, labelMap[e._id.$oid]); + return activeFilter?.filter(e, timelineLabelMap[e._id.$oid]); }); /* next, filter out any untracked time if the trips that came before and after it are no longer displayed */ @@ -100,7 +102,7 @@ const LabelTab = () => { logDebug('No active filter, displaying all entries'); } setDisplayedEntries(entriesToDisplay); - } + }, [timelineMap, filterInputs, timelineLabelMap, filterRefreshTs]); // once pipelineRange is set, update all unprocessed inputs useEffect(() => { diff --git a/www/js/diary/addressNamesHelper.ts b/www/js/diary/addressNamesHelper.ts index 30740ad19..de3db47ea 100644 --- a/www/js/diary/addressNamesHelper.ts +++ b/www/js/diary/addressNamesHelper.ts @@ -75,6 +75,7 @@ export function useLocalStorage(key: string, initialValue: T) { import Bottleneck from 'bottleneck'; import { displayError, logDebug } from '../plugin/logger'; +import { CompositeTrip } from '../types/diaryTypes'; let nominatimLimiter = new Bottleneck({ maxConcurrent: 2, minTime: 500 }); export const resetNominatimLimiter = () => { @@ -137,7 +138,7 @@ async function fetchNominatimLocName(loc_geojson) { } // Schedules nominatim fetches for the start and end locations of a trip -export function fillLocationNamesOfTrip(trip) { +export function fillLocationNamesOfTrip(trip: CompositeTrip) { nominatimLimiter.schedule(() => fetchNominatimLocName(trip.end_loc)); nominatimLimiter.schedule(() => fetchNominatimLocName(trip.start_loc)); } diff --git a/www/js/diary/list/LabelListScreen.tsx b/www/js/diary/list/LabelListScreen.tsx index 54df33500..af27bfe00 100644 --- a/www/js/diary/list/LabelListScreen.tsx +++ b/www/js/diary/list/LabelListScreen.tsx @@ -12,8 +12,13 @@ import { displayErrorMsg } from '../../plugin/logger'; const LabelListScreen = () => { const { filterInputs, setFilterInputs, displayedEntries } = useContext(LabelTabContext); - const { timelineMap, loadSpecificWeek, timelineIsLoading, refreshTimeline } = - useContext(TimelineContext); + const { + timelineMap, + loadSpecificWeek, + timelineIsLoading, + refreshTimeline, + shouldUpdateTimeline, + } = useContext(TimelineContext); const { colors } = useTheme(); return ( @@ -42,7 +47,7 @@ const LabelListScreen = () => { /> - + {shouldUpdateTimeline && } ); diff --git a/www/js/diary/useDerivedProperties.tsx b/www/js/diary/useDerivedProperties.tsx index 06a870fe8..f13c1862d 100644 --- a/www/js/diary/useDerivedProperties.tsx +++ b/www/js/diary/useDerivedProperties.tsx @@ -8,6 +8,7 @@ import { getLocalTimeString, getDetectedModes, isMultiDay, + primarySectionForTrip, } from './diaryHelper'; import TimelineContext from '../TimelineContext'; @@ -24,6 +25,7 @@ const useDerivedProperties = (tlEntry) => { return { confirmedMode: confirmedModeFor(tlEntry), + primary_ble_sensed_mode: primarySectionForTrip(tlEntry)?.ble_sensed_mode?.baseMode, displayDate: getFormattedDate(beginFmt, endFmt), displayStartTime: getLocalTimeString(beginDt), displayEndTime: getLocalTimeString(endDt),