Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 5 additions & 80 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react';

import Ionicons from '@expo/vector-icons/Ionicons';
import {SelectProvider} from '@mobile-reality/react-native-select-pro';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {getStateFromPath, NavigationContainer, PathConfigMap, RouteProp, useNavigationContainerRef} from '@react-navigation/native';
import {getStateFromPath, NavigationContainer, PathConfigMap, useNavigationContainerRef} from '@react-navigation/native';
import * as SplashScreen from 'expo-splash-screen';
import {ActivityIndicator, AppState, AppStateStatus, Image, Platform, StatusBar, StyleSheet, View} from 'react-native';
import {SafeAreaProvider} from 'react-native-safe-area-context';
Expand All @@ -14,19 +12,13 @@ import * as BackgroundFetch from 'expo-background-task';
import Constants from 'expo-constants';
import * as TaskManager from 'expo-task-manager';

import {merge} from 'lodash';

import AsyncStorage from '@react-native-async-storage/async-storage';
import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister';
import {focusManager, QueryCache, QueryClient, useQueryClient} from '@tanstack/react-query';
import {PersistQueryClientProvider} from '@tanstack/react-query-persist-client';

import {ClientContext, ClientProps, productionHosts, stagingHosts} from 'clientContext';
import {ActionToast, ErrorToast, InfoToast, SuccessToast, WarningToast} from 'components/content/Toast';
import {HomeTabScreen} from 'components/screens/HomeScreen';
import {MenuStackScreen} from 'components/screens/MenuScreen';
import {ObservationsTabScreen} from 'components/screens/ObservationsScreen';
import {WeatherScreen} from 'components/screens/WeatherScreen';
import {HTMLRendererConfig} from 'components/text/HTML';
import ImageCache, {queryKeyPrefix} from 'hooks/useCachedImageURI';
import {useOnlineManager} from 'hooks/useOnlineManager';
Expand All @@ -35,8 +27,6 @@ import {logFilePath, logger} from 'logger';
import {LoggerContext, LoggerProps} from 'loggerContext';
import {prefetchAllActiveForecasts} from 'network/prefetchAllActiveForecasts';
import Toast, {ToastConfigParams} from 'react-native-toast-message';
import {TabNavigatorParamList} from 'routes';
import {colorLookup} from 'theme';
import {AvalancheCenterWebsites} from 'types/nationalAvalancheCenter';

import 'date-time-format-timezone';
Expand All @@ -46,7 +36,7 @@ import {QUERY_CACHE_ASYNC_STORAGE_KEY} from 'data/asyncStorageKeys';
import * as FileSystem from 'expo-file-system';
import {PreferencesProvider, usePreferences} from 'Preferences';
import {NotFoundError} from 'types/requests';
import {formatRequestedTime, RequestedTime} from 'utils/date';
import {RequestedTime} from 'utils/date';

import Mapbox from '@rnmapbox/maps';
import {Integration} from '@sentry/types';
Expand All @@ -55,6 +45,7 @@ import * as messages from 'compiled-lang/en.json';
import {Button} from 'components/content/Button';
import {Center, VStack} from 'components/core';
import {KillSwitchMonitor} from 'components/KillSwitchMonitor';
import {DrawerNavigator} from 'components/screens/navigation/Drawer';
import {Body, BodyBlack, Title3Black} from 'components/text';
import * as Linking from 'expo-linking';
import * as Updates from 'expo-updates';
Expand Down Expand Up @@ -237,8 +228,6 @@ const asyncStoragePersister = createAsyncStoragePersister({
key: QUERY_CACHE_ASYNC_STORAGE_KEY,
});

const TabNavigator = createBottomTabNavigator<TabNavigatorParamList>();

// We add the listener at startup and never plan to stop listening, so there's
// no need to unsubscribe here.
AppState.addEventListener('change', (status: AppStateStatus) => {
Expand Down Expand Up @@ -367,27 +356,6 @@ const BaseApp: React.FunctionComponent<{
});
}, [setUpdateStatus, logger]);

const tabNavigatorScreenOptions = useCallback(
({route: {name}}: {route: RouteProp<TabNavigatorParamList, keyof TabNavigatorParamList>}) => ({
headerShown: false,
tabBarIcon: ({color, size}: {focused: boolean; color: string; size: number}) => {
if (name === 'Home') {
return <Ionicons name="map-outline" size={size} color={color} />;
} else if (name === 'Observations') {
return <Ionicons name="reader-outline" size={size} color={color} />;
} else if (name === 'Weather Data') {
return <Ionicons name="stats-chart-outline" size={size} color={color} />;
} else if (name === 'Menu') {
return <Ionicons name="ellipsis-horizontal" size={size} color={color} />;
}
},
// these two properties should really take ColorValue but oh well
tabBarActiveTintColor: colorLookup('primary') as string,
tabBarInactiveTintColor: colorLookup('text.secondary') as string,
}),
[],
);

const [startupPaused, {off: unpauseStartup}] = useToggle(process.env.EXPO_PUBLIC_PAUSE_ON_STARTUP === 'true');

if (updateStatus !== 'ready' || preferences.mixpanelUserId == '') {
Expand Down Expand Up @@ -514,52 +482,9 @@ const BaseApp: React.FunctionComponent<{
<FeatureFlagsProvider>
<KillSwitchMonitor>
<SelectProvider>
<StatusBar barStyle="dark-content" backgroundColor="white" />
<StatusBar barStyle={'dark-content'} animated={false} backgroundColor={'white'} />
<View style={{flex: 1}}>
<TabNavigator.Navigator initialRouteName="Home" screenOptions={tabNavigatorScreenOptions}>
<TabNavigator.Screen name="Home" initialParams={{requestedTime: formatRequestedTime(requestedTime)}} options={{title: 'Zones'}}>
{state =>
HomeTabScreen(
merge(state, {
route: {
params: {
requestedTime: formatRequestedTime(requestedTime),
},
},
}),
)
}
</TabNavigator.Screen>
<TabNavigator.Screen name="Observations" initialParams={{requestedTime: formatRequestedTime(requestedTime)}}>
{state =>
ObservationsTabScreen(
merge(state, {
route: {
params: {
requestedTime: formatRequestedTime(requestedTime),
},
},
}),
)
}
</TabNavigator.Screen>
<TabNavigator.Screen name="Weather Data" initialParams={{requestedTime: formatRequestedTime(requestedTime)}}>
{state =>
WeatherScreen(
merge(state, {
route: {
params: {
requestedTime: formatRequestedTime(requestedTime),
},
},
}),
)
}
</TabNavigator.Screen>
<TabNavigator.Screen name="Menu" initialParams={{requestedTime: formatRequestedTime(requestedTime)}} options={{title: 'More'}}>
{state => MenuStackScreen(state, queryCache, staging, setStaging)}
</TabNavigator.Screen>
</TabNavigator.Navigator>
<DrawerNavigator requestedTime={requestedTime} centerId={avalancheCenterId} staging={staging} setStaging={setStaging} />
</View>
</SelectProvider>
</KillSwitchMonitor>
Expand Down
18 changes: 9 additions & 9 deletions components/AvalancheCenterSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,25 @@ import {incompleteQueryState, QueryState} from 'components/content/QueryState';
import {useAllAvalancheCenterMetadata} from 'hooks/useAllAvalancheCenterMetadata';
import {useAvalancheCenterCapabilities} from 'hooks/useAvalancheCenterCapabilities';
import {usePostHog} from 'posthog-react-native';
import {MenuStackParamList, TabNavigationProps} from 'routes';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {MainStackNavigationProps, MainStackParamList} from 'routes';
import {AvalancheCenter, AvalancheCenterID} from 'types/nationalAvalancheCenter';

export const AvalancheCenterSelector: React.FunctionComponent<{
currentCenterId: AvalancheCenterID;
setAvalancheCenter: (center: AvalancheCenterID) => void;
}> = ({currentCenterId, setAvalancheCenter}) => {
const navigation = useNavigation<TabNavigationProps>();
const route = useRoute<NativeStackScreenProps<MenuStackParamList, 'avalancheCenterSelector'>['route']>();
const navigation = useNavigation<MainStackNavigationProps>();
const route = useRoute<NativeStackScreenProps<MainStackParamList, 'avalancheCenterSelector'>['route']>();
const capabilitiesResult = useAvalancheCenterCapabilities();
const capabilities = capabilitiesResult.data;
const whichCenters = route.params.debugMode ? AvalancheCenters.AllCenters : AvalancheCenters.SupportedCenters;
const metadataResults = useAllAvalancheCenterMetadata(capabilities, whichCenters);
const setAvalancheCenterWrapper = React.useCallback(
(center: AvalancheCenterID) => {
setAvalancheCenter(center);
// We need to clear navigation state to force all screens from the
// previous avalanche center selection to unmount
navigation.reset({
index: 0,
routes: [{name: 'Home'}],
});

navigation.goBack();
},
[setAvalancheCenter, navigation],
);
Expand All @@ -42,6 +39,8 @@ export const AvalancheCenterSelector: React.FunctionComponent<{
}, [postHog]);
useFocusEffect(recordAnalytics);

const insets = useSafeAreaInsets();

if (incompleteQueryState(capabilitiesResult, ...metadataResults) || !capabilities) {
return <QueryState results={[capabilitiesResult, ...metadataResults]} />;
}
Expand All @@ -64,6 +63,7 @@ export const AvalancheCenterSelector: React.FunctionComponent<{
bg="white"
px={16}
py={8}
paddingBottom={insets.bottom}
/>
</ScrollView>
);
Expand Down
56 changes: 24 additions & 32 deletions components/AvalancheForecastZoneMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,21 @@ import {useMapLayerAvalancheWarnings} from 'hooks/useMapLayerAvalancheWarnings';
import {LoggerContext, LoggerProps} from 'loggerContext';
import {usePostHog} from 'posthog-react-native';
import {usePreferences} from 'Preferences';
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
import {HomeStackNavigationProps, TabNavigationProps} from 'routes';
import {SafeAreaView} from 'react-native-safe-area-context';
import {MainStackNavigationProps, TabNavigationProps} from 'routes';
import {AvalancheCenterID, DangerLevel, ForecastPeriod, isSupportedCenter, MapLayerFeature, ProductType} from 'types/nationalAvalancheCenter';
import {formatRequestedTime, RequestedTime, requestedTimeToUTCDate, utcDateToLocalTimeString} from 'utils/date';

import {Camera, MapState} from '@rnmapbox/maps';
import {defaultMapRegionForGeometries} from 'components/helpers/geographicCoordinates';
import {useAllMapLayers} from 'hooks/useAllMapLayers';
import Toast from 'react-native-toast-message';

export interface MapProps {
center_id: AvalancheCenterID;
requestedTime: RequestedTime;
}

export const AvalancheForecastZoneMap: React.FunctionComponent<MapProps> = ({requestedTime}: MapProps) => {
export const AvalancheForecastZoneMap: React.FunctionComponent<MapProps> = ({center_id, requestedTime}: MapProps) => {
const {logger} = React.useContext<LoggerProps>(LoggerContext);

const {preferences, setPreferences} = usePreferences();
Expand All @@ -46,27 +46,25 @@ export const AvalancheForecastZoneMap: React.FunctionComponent<MapProps> = ({req
const allMapLayersResult = useAllMapLayers();
const allMapLayers = allMapLayersResult.data;

const metadataResult = useAvalancheCenterMetadata(center);
const metadataResult = useAvalancheCenterMetadata(center_id);
const metadata = metadataResult.data;
const forecastResults = useMapLayerAvalancheForecasts(center, requestedTime, allMapLayers, metadata);
const warningResults = useMapLayerAvalancheWarnings(center, requestedTime, allMapLayers);
const forecastResults = useMapLayerAvalancheForecasts(center_id, requestedTime, allMapLayers, metadata);
const warningResults = useMapLayerAvalancheWarnings(center_id, requestedTime, allMapLayers);

const navigation = useNavigation<HomeStackNavigationProps & TabNavigationProps>();
const navigation = useNavigation<MainStackNavigationProps & TabNavigationProps>();
const [selectedZoneId, setSelectedZoneId] = useState<number | null>(null);

const topElements = React.useRef<RNView>(null);

const insets = useSafeAreaInsets();

const postHog = usePostHog();

const recordAnalytics = useCallback(() => {
if (postHog && center) {
if (postHog && center_id) {
postHog.screen('avalancheForecastMap', {
center: center,
center: center_id,
});
}
}, [postHog, center]);
}, [postHog, center_id]);
useFocusEffect(recordAnalytics);

const onMapPresOutsideOfPolygon = useCallback(
Expand All @@ -89,14 +87,8 @@ export const AvalancheForecastZoneMap: React.FunctionComponent<MapProps> = ({req
const selectedZoneCenter = zone.center_id;
if (isSupportedCenter(selectedZoneCenter)) {
setSelectedZoneId(zone.zone_id);
if (selectedZoneCenter !== center) {
if (selectedZoneCenter !== center_id) {
setPreferences({center: selectedZoneCenter});
// This is purely for debugging purposes
Toast.show({
type: 'info',
text1: `Your preferred center has changed to: ${selectedZoneCenter}`,
position: 'bottom',
});
}
} else {
Alert.alert(`${selectedZoneCenter} is not supported`, `Please go to their website to view the full forecast for ${selectedZoneCenter} or select another center`, [
Expand All @@ -112,17 +104,23 @@ export const AvalancheForecastZoneMap: React.FunctionComponent<MapProps> = ({req
}
}
},
[navigation, selectedZoneId, center, requestedTime, setSelectedZoneId, setPreferences],
[navigation, selectedZoneId, center_id, requestedTime, setSelectedZoneId, setPreferences],
);

const preferredCenterFeatures = useMemo(() => allMapLayers?.features.filter(feature => feature.properties.center_id === center), [allMapLayers, center]);
const preferredCenterFeatures = useMemo(() => allMapLayers?.features.filter(feature => feature.properties.center_id === center_id), [allMapLayers, center_id]);

const avalancheCenterMapRegion = useMemo(() => defaultMapRegionForGeometries(preferredCenterFeatures?.map(feature => feature.geometry)), [preferredCenterFeatures]);

// useRef has to be used here. Animation and gesture handlers can't use props and state,
// and aren't re-evaluated on render. Fun!
const mapCameraRef = useRef<Camera>(null);
const controller = useRef<AnimatedMapWithDrawerController>(new AnimatedMapWithDrawerController(AnimatedDrawerState.Hidden, avalancheCenterMapRegion, mapCameraRef, logger));

const reanimateOnFocus = useCallback(() => {
controller.current.forceAnimateMapRegion();
}, [controller]);
useFocusEffect(reanimateOnFocus);

React.useEffect(() => {
controller.current.animateUsingUpdatedAvalancheCenterMapRegion(avalancheCenterMapRegion);
}, [avalancheCenterMapRegion, controller]);
Expand Down Expand Up @@ -157,14 +155,8 @@ export const AvalancheForecastZoneMap: React.FunctionComponent<MapProps> = ({req
const onSelectCenter = useCallback(
(center: AvalancheCenterID) => {
setPreferences({center: center, hasSeenCenterPicker: true});
// We need to clear navigation state to force all screens from the
// previous avalanche center selection to unmount
navigation.reset({
index: 0,
routes: [{name: 'Home'}],
});
},
[setPreferences, navigation],
[setPreferences],
);

const onCameraChanged = useCallback(
Expand Down Expand Up @@ -270,7 +262,7 @@ export const AvalancheForecastZoneMap: React.FunctionComponent<MapProps> = ({req

const zones = useMemo(() => (zonesById !== undefined ? Object.keys(zonesById).map(k => zonesById[k]) : []), [zonesById]);

const selectedACZones = useMemo(() => zones.filter(zone => zone.feature.properties.center_id === center), [zones, center]);
const selectedACZones = useMemo(() => zones.filter(zone => zone.feature.properties.center_id === center_id), [zones, center_id]);

const showAvalancheCenterSelectionModal = useMemo(() => !preferences.hasSeenCenterPicker, [preferences.hasSeenCenterPicker]);

Expand Down Expand Up @@ -306,7 +298,7 @@ export const AvalancheForecastZoneMap: React.FunctionComponent<MapProps> = ({req
onCameraChanged={onCameraChanged}
/>

<VStack ref={topElements} width="100%" position="absolute" paddingTop={insets.top} left={0} right={0} mt={8} px={4} flex={1} onLayout={onLayout}>
<VStack ref={topElements} width="100%" position="absolute" left={0} right={0} mt={8} px={4} flex={1} onLayout={onLayout}>
<DangerScale width="100%" />
</VStack>

Expand Down Expand Up @@ -349,7 +341,7 @@ const AvalancheForecastZoneCard: React.FunctionComponent<{
zone: MapViewZone;
}> = React.memo(({date, zone}: {date: RequestedTime; zone: MapViewZone}) => {
const {width} = useWindowDimensions();
const navigation = useNavigation<HomeStackNavigationProps>();
const navigation = useNavigation<MainStackNavigationProps>();

const dangerLevel = zone.danger_level ?? DangerLevel.None;
const dangerColor = colorFor(dangerLevel);
Expand Down
4 changes: 2 additions & 2 deletions components/FeatureFlagsDebugger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {Body, BodySmBlack, Caption1Semibold} from 'components/text';
import React, {useCallback, useEffect, useState} from 'react';
import {FlatList, StyleSheet, Switch} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import {MenuStackParamList} from 'routes';
import {MainStackParamList} from 'routes';
import {colorLookup} from 'theme';

export const FeatureFlagsDebuggerScreen = (_: NativeStackScreenProps<MenuStackParamList, 'featureFlags'>) => {
export const FeatureFlagsDebuggerScreen = (_: NativeStackScreenProps<MainStackParamList, 'featureFlags'>) => {
return (
<SafeAreaView style={StyleSheet.absoluteFillObject} edges={['top', 'left', 'right']}>
<FeatureFlagsDebugger />
Expand Down
Loading