From 3aa2ce505bc81215d089ffc4b18f10597a27a462 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 21 May 2024 12:02:31 -0400 Subject: [PATCH 1/4] fix address names not loading 'timelineMap' is a Map object, not just a plain { } object. So instead of Object.values() we should use Map.values() - which returns an iterator - but we want to reverse it. So we have to convert to array, then reverse, and then call forEach to fillLocationNamesOfTrip. Also, we should ensure that only trips, not places, get passed to fillLocationNamesOfTrip (since places always have the same locations as the trips surrounding them, the place cards will automatically get their location names filled in) --- www/js/diary/LabelTab.tsx | 12 ++++++------ www/js/diary/addressNamesHelper.ts | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index f619fbf4e..705194873 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -15,7 +15,7 @@ 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 { TimelineEntry, isTrip } from '../types/diaryTypes'; import TimelineContext, { LabelTabFilter, TimelineLabelMap } from '../TimelineContext'; import { AppContext } from '../App'; import { subscribe } from '../customEventHandler'; @@ -58,11 +58,11 @@ const LabelTab = () => { 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) { 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)); } From 0cc4a14344b4ba87f9e8155a8fd9546c3d2bd307 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Thu, 25 Apr 2024 17:15:05 -0700 Subject: [PATCH 2/4] update TimelineScrollList only when the active tab is 'label' Leaflet map encounters an error when prerendered, so we need to render the TimelineScrollList component when the active tab is 'label' 'shouldUpdateTimeline' state is used to determine whether to render the TimelineScrollList or not --- www/js/Main.tsx | 8 +++++++- www/js/TimelineContext.ts | 7 +++++++ www/js/diary/list/LabelListScreen.tsx | 11 ++++++++--- 3 files changed, 22 insertions(+), 4 deletions(-) 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 +71,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(() => { @@ -356,6 +361,8 @@ export const useTimelineContext = (): ContextProps => { notesFor, confirmedModeFor, addUserInputToEntry, + shouldUpdateTimeline, + setShouldUpdateTimeline, }; }; 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 && } ); From db893d54da614107727050684350c75c03046e20 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 21 May 2024 13:55:54 -0400 Subject: [PATCH 3/4] fix LabelTab filtering The existing implementation used pub/sub events to trigger when filtering should happen. The events were triggered from TimelineContext. However, this doesn't listen / wait for filterInputs to be defined because filterInputs is inside LabelTab, not TimelineContext. Filtering wasn't working because filterInputs was still empty when the initial filtering occured and LabelTab had no control over when the event was triggered. A better solution is to listen for changes inside LabelTab. Every time the filters, the loaded trips, or the labels change, filtering occurs. If a trip had a user input in the last 30s, we skip it; but schedule a re-filter when its "immunity" expires. Now LabelTab is responsible for all the filtering. TimelineContext doesn't have to worry about what LabelTab (its child) does. This is a cleaner, "React-recommended" unidirectional pattern --- www/js/TimelineContext.ts | 13 ------------ www/js/diary/LabelTab.tsx | 44 ++++++++++++++++++++------------------- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/www/js/TimelineContext.ts b/www/js/TimelineContext.ts index 347403e7d..aa12dacac 100644 --- a/www/js/TimelineContext.ts +++ b/www/js/TimelineContext.ts @@ -21,7 +21,6 @@ import { } from './diary/timelineHelper'; import { getPipelineRangeTs } from './services/commHelper'; import { getNotDeletedCandidates, mapInputsToTimelineEntries } from './survey/inputMatcher'; -import { publish } from './customEventHandler'; import { EnketoUserInputEntry } from './survey/enketo/enketoHelper'; import { VehicleIdentity } from './types/appConfigTypes'; import { primarySectionForTrip } from './diary/diaryHelper'; @@ -140,10 +139,6 @@ export const useTimelineContext = (): ContextProps => { ); setTimelineLabelMap(newTimelineLabelMap); setTimelineNotesMap(newTimelineNotesMap); - publish('applyLabelTabFilters', { - timelineMap, - timelineLabelMap: newTimelineLabelMap, - }); setTimelineIsLoading(false); }, [timelineMap]); @@ -321,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 } }; diff --git a/www/js/diary/LabelTab.tsx b/www/js/diary/LabelTab.tsx index 705194873..ef8507559 100644 --- a/www/js/diary/LabelTab.tsx +++ b/www/js/diary/LabelTab.tsx @@ -16,9 +16,8 @@ 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, isTrip } from '../types/diaryTypes'; -import TimelineContext, { LabelTabFilter, TimelineLabelMap } from '../TimelineContext'; +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,17 +43,12 @@ 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(() => { @@ -65,24 +60,31 @@ const LabelTab = () => { }); }, [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(() => { From 39d463c5926116bbf6e165b92f6794111b4dcfab Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 20 May 2024 17:17:47 -0400 Subject: [PATCH 4/4] include primary_ble_sensed_mode in derived properties The 'showsIf' conditions in dfc-fermata currently reference 'confirmedMode'. We want to rename this to 'primary_ble_sensed_mode'. Also see https://github.com/JGreenlee/e-mission-common/commit/039633992a16c78a86713fc4a4a3735075c74527 --- www/js/diary/useDerivedProperties.tsx | 2 ++ 1 file changed, 2 insertions(+) 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),