Skip to content

Commit

Permalink
prevent UI loading in background; refactor useAppState
Browse files Browse the repository at this point in the history
```
    // On iOS, the UI can run if the app is launched by the OS in response to a notification,
    // in which case the appState will be 'background'. In this case, we definitely do not want
    // to load the UI because it is not visible.
```

`useAppStateChange` only accepted a callback for onResume. Refactored this into a more versatile hook `useAppState`, which still allows optional onResume or onChange callbacks and also returns the underlying appState.

This allows App.tsx to useAppState and return null if appState is not 'active'. in this case the App.tsx itself will still initialize but it will not have any children

TimelineContext will no longer be rendered while the app is in the background so it no longer needs to listen for app state changes.
usePermissionStatus will still listen for onResume via the new interface, since it is invoked in App.tsx.
  • Loading branch information
JGreenlee committed Oct 25, 2024
1 parent b130928 commit 8971f05
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 16 deletions.
16 changes: 16 additions & 0 deletions www/js/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import AlertBar from './components/AlertBar';
import Main from './Main';
import { joinWithTokenOrUrl } from './config/dynamicConfig';
import { addStatReading } from './plugin/clientStats';
import useAppState from './useAppState';
import { displayErrorMsg, logDebug } from './plugin/logger';
import i18next from 'i18next';

Expand Down Expand Up @@ -75,6 +76,21 @@ const App = () => {
// getUserCustomLabels(CUSTOM_LABEL_KEYS_IN_DATABASE).then((res) => setCustomLabelMap(res));
}, [appConfig]);

const appState = useAppState({});
if (appState != 'active') {
// Render nothing if the app state is not 'active'.
// On iOS, the UI can run if the app is launched by the OS in response to a notification,
// in which case the appState will be 'background'. In this case, we definitely do not want
// to load the UI because it is not visible.
// On Android, the UI can only be initiated by the user - but even so, the user can send it to
// the background and we don't need the UI to stay active.
// In the future, we may want to persist some UI states when the app is sent to the background;
// i.e. the user opens the app, navigates away, and back again.
// But currently, we're relying on a 'fresh' UI every time the app goes to 'active' state.
logDebug(`App: appState = ${appState}; returning null`);
return null;
}

const appContextValue = {
appConfig,
handleTokenOrUrl,
Expand Down
2 changes: 0 additions & 2 deletions www/js/TimelineContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { getPipelineRangeTs } from './services/commHelper';
import { getNotDeletedCandidates, mapInputsToTimelineEntries } from './survey/inputMatcher';
import { EnketoUserInputEntry } from './survey/enketo/enketoHelper';
import { primarySectionForTrip } from './diary/diaryHelper';
import useAppStateChange from './useAppStateChange';
import { isoDateRangeToTsRange, isoDateWithOffset } from './datetimeUtil';
import { base_modes } from 'e-mission-common';

Expand Down Expand Up @@ -55,7 +54,6 @@ type ContextProps = {
export const useTimelineContext = (): ContextProps => {
const { t } = useTranslation();
const appConfig = useAppConfig();
useAppStateChange(() => refreshTimeline());

const [labelOptions, setLabelOptions] = useState<LabelOptions | null>(null);
// timestamp range that has been processed by the pipeline on the server
Expand Down
24 changes: 14 additions & 10 deletions www/js/useAppStateChange.ts → www/js/useAppState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,30 @@
//the executes "onResume" function that is passed in
//https://reactnative.dev/docs/appstate based on react's example of detecting becoming active

import { useEffect, useRef } from 'react';
import { useEffect, useState } from 'react';
import { AppState } from 'react-native';
import { addStatReading } from './plugin/clientStats';

const useAppStateChange = (onResume) => {
const appState = useRef(AppState.currentState);
type Props = {
onResume?: () => void;
onChange?: (nextAppState: string) => void;
};
const useAppState = ({ onResume, onChange }: Props) => {
const [appState, setAppState] = useState(AppState.currentState);

useEffect(() => {
const subscription = AppState.addEventListener('change', (nextAppState) => {
if (appState.current != 'active' && nextAppState === 'active') {
onResume();
addStatReading('app_state_change', nextAppState);
onChange?.(nextAppState);
if (nextAppState == 'active' && appState != 'active') {
onResume?.();
}

appState.current = nextAppState;
addStatReading('app_state_change', appState.current);
setAppState(nextAppState);
});
return () => subscription.remove();
}, []);

return {};
return appState;
};

export default useAppStateChange;
export default useAppState;
10 changes: 6 additions & 4 deletions www/js/usePermissionStatus.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState, useMemo } from 'react';
import useAppStateChange from './useAppStateChange';
import useAppState from './useAppState';
import useAppConfig from './useAppConfig';
import { useTranslation } from 'react-i18next';
import { useAppTheme } from './appTheme';
Expand Down Expand Up @@ -358,9 +358,11 @@ const usePermissionStatus = () => {
refreshAllChecks(checkList);
}

useAppStateChange(() => {
logDebug('PERMISSION CHECK: app has resumed, should refresh');
refreshAllChecks(checkList);
useAppState({
onResume: () => {
logDebug('PERMISSION CHECK: app has resumed, should refresh');
refreshAllChecks(checkList);
},
});

//load when ready
Expand Down

0 comments on commit 8971f05

Please sign in to comment.