Skip to content

Commit

Permalink
feat: Add notifications UI (#439)
Browse files Browse the repository at this point in the history
* Start notifications

* Add lottie

* Add old format lottie

* New animation

* Re put 50

* Small tweaks

* Update animation

* Make notifications work with backend

* Star tiwth new design

* CircleButtons

* Change Location uses CircleButton

* CodeClimate

* Make it pretty on iOS

* Same ActionSheet on both platform

* Make CircleButton code cleaner

* Separate ActionPicker

* Fix scroll on home screen

* Add notifications permission

* Make everything work

* Put back staging
  • Loading branch information
amaury1093 committed Feb 9, 2020
1 parent edfe91e commit fc9bffc
Show file tree
Hide file tree
Showing 46 changed files with 1,042 additions and 648 deletions.
23 changes: 13 additions & 10 deletions App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// along with Sh**t! I Smoke. If not, see <http://www.gnu.org/licenses/>.

import { ApolloProvider } from '@apollo/react-hooks';
import { ActionSheetProvider } from '@expo/react-native-action-sheet';
import Constants from 'expo-constants';
import * as Font from 'expo-font';
import React, { useEffect, useState } from 'react';
Expand Down Expand Up @@ -76,16 +77,18 @@ export function App(): React.ReactElement {
<ErrorContextProvider>
<LocationContextProvider>
<ApolloProvider client={client}>
<ApiContextProvider>
<FrequencyContextProvider>
<DistanceUnitProvider>
{Platform.select({
ios: <StatusBar barStyle="dark-content" />
})}
<Screens />
</DistanceUnitProvider>
</FrequencyContextProvider>
</ApiContextProvider>
<ActionSheetProvider>
<ApiContextProvider>
<FrequencyContextProvider>
<DistanceUnitProvider>
{Platform.select({
ios: <StatusBar barStyle="dark-content" />
})}
<Screens />
</DistanceUnitProvider>
</FrequencyContextProvider>
</ApiContextProvider>
</ActionSheetProvider>
</ApolloProvider>
</LocationContextProvider>
</ErrorContextProvider>
Expand Down
33 changes: 19 additions & 14 deletions App/Screens/Home/Footer/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,27 @@ import React, { useContext } from 'react';
import { StyleSheet, View, ViewProps } from 'react-native';
import { NavigationInjectedProps } from 'react-navigation';

import { Button } from '../../../components';
import { Button, CircleButton } from '../../../components';
import { i18n } from '../../../localization';
import { ApiContext, CurrentLocationContext } from '../../../stores';
import { track } from '../../../util/amplitude';
import { isStationTooFar } from '../../../util/station';
import * as theme from '../../../util/theme';
import { aboutSections } from '../../About';
import { SelectNotifications } from './SelectNotifications';
import { ShareButton } from './ShareButton';

interface FooterProps extends NavigationInjectedProps, ViewProps {}

const styles = StyleSheet.create({
smallButtons: {
secondLine: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-around',
marginTop: theme.spacing.mini
justifyContent: 'space-between',
marginTop: theme.spacing.small
},
share: {
marginRight: theme.spacing.small
}
});

Expand Down Expand Up @@ -86,25 +91,25 @@ export function Footer(props: FooterProps): React.ReactElement {

function renderSmallButtons(): React.ReactElement {
return (
<View style={styles.smallButtons}>
<>
<ShareButton style={styles.share} />
{isTooFar ? (
<Button icon="plus-circle" onPress={goToDetails} type="secondary">
{i18n.t('home_btn_more_details').toUpperCase()}
</Button>
<CircleButton onPress={goToDetails} text="DET" />
) : (
<Button icon="question-circle" onPress={goToAbout} type="secondary">
{i18n.t('home_btn_faq_about').toUpperCase()}
</Button>
<CircleButton onPress={goToAbout} text="FAQ" />
)}
<ShareButton />
</View>
</>
);
}

return (
<View style={[theme.withPadding, style]} {...rest}>
{renderBigButton()}
{renderSmallButtons()}
<View style={styles.secondLine}>
<SelectNotifications />
<View style={{ flexGrow: 1 }} />
{renderSmallButtons()}
</View>
</View>
);
}
223 changes: 223 additions & 0 deletions App/Screens/Home/Footer/SelectNotifications/SelectNotifications.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// Sh**t! I Smoke
// Copyright (C) 2018-2020 Marcelo S. Coelho, Amaury Martiny

// Sh**t! I Smoke is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Sh**t! I Smoke is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Sh**t! I Smoke. If not, see <http://www.gnu.org/licenses/>.

import { FontAwesome } from '@expo/vector-icons';
import { Frequency } from '@shootismoke/graphql';
import { Notifications } from 'expo';
import * as Localization from 'expo-localization';
import * as Permissions from 'expo-permissions';
import { pipe } from 'fp-ts/lib/pipeable';
import * as T from 'fp-ts/lib/Task';
import * as TE from 'fp-ts/lib/TaskEither';
import React, { useContext, useEffect, useState } from 'react';
import { AsyncStorage, StyleSheet, Text, View, ViewProps } from 'react-native';
import { scale } from 'react-native-size-matters';
import Switch from 'react-native-switch-pro';

import { ActionPicker } from '../../../../components';
import { i18n } from '../../../../localization';
import { ApiContext } from '../../../../stores';
import { updateNotifications } from '../../../../stores/util';
import { logFpError, promiseToTE } from '../../../../util/fp';
import * as theme from '../../../../util/theme';

const STORAGE_KEY = 'NOTIFICATIONS';

const notificationsValues = ['never', 'daily', 'weekly', 'monthly'] as const;

/**
* Capitalize a string.
*
* @param s - String to capitalize
*/
function capitalize(s: string): string {
return s[0].toUpperCase() + s.slice(1);
}

/**
* Convert hex to rgba.
* @see https://stackoverflow.com/questions/21646738/convert-hex-to-rgba#answer-51564734
*/
function hex2rgba(hex: string, alpha = 1): string {
const matches = hex.match(/\w\w/g);
if (!matches) {
throw new Error(`Invalid hex: ${hex}`);
}

const [r, g, b] = matches.map(x => parseInt(x, 16));

return `rgba(${r},${g},${b},${alpha})`;
}

type SelectNotificationsProps = ViewProps;

const styles = StyleSheet.create({
container: {
alignItems: 'center',
flexDirection: 'row'
},
label: {
...theme.text,
textTransform: 'uppercase'
},
labelFrequency: {
...theme.text,
color: theme.primaryColor,
fontFamily: theme.gothamBlack,
fontWeight: '900',
textTransform: 'uppercase'
},
switch: {
marginRight: theme.spacing.small
}
});

export function SelectNotifications(
props: SelectNotificationsProps
): React.ReactElement {
const { style, ...rest } = props;
const [notif, setNotif] = useState<Frequency>('never');
const { api } = useContext(ApiContext);

useEffect(() => {
async function getNotifications(): Promise<void> {
const value = await AsyncStorage.getItem(STORAGE_KEY);

if (value && notificationsValues.includes(value as Frequency)) {
setNotif(value as Frequency);
}
}

getNotifications();
}, []);

/**
* Handler for changing notification frequency
*
* @param buttonIndex - The button index in the ActionSheet
*/
function handleChangeNotif(frequency: Frequency): void {
setNotif(frequency);

if (!api) {
throw new Error(
'Home/SelectNotifications/SelectNotifications.tsx only gets displayed when `api` is defined.'
);
}

pipe(
promiseToTE(async () => {
const { status } = await Permissions.askAsync(
Permissions.NOTIFICATIONS
);

if (status !== 'granted') {
throw new Error('Permission to access notifications was denied');
}

return await Notifications.getExpoPushTokenAsync();
}, 'SelectNotifications'),
TE.chain(expoPushToken =>
updateNotifications({
expoPushToken,
frequency,
station: api.pm25.location,
timezone: Localization.timezone
})
),
TE.chain(() =>
promiseToTE(
() => AsyncStorage.setItem(STORAGE_KEY, frequency),
'SelectNotifications'
)
),
TE.fold(
() => {
setNotif('never');
AsyncStorage.setItem(STORAGE_KEY, 'never');

return T.of(void undefined);
},
() => T.of(void undefined)
)
)().catch(logFpError('SelectNotifications'));
}

// Is the switch on or off?
const isSwitchOn = notif !== 'never';

return (
<View style={[styles.container, style]} {...rest}>
<Switch
backgroundActive={theme.primaryColor}
backgroundInactive={hex2rgba(
theme.secondaryTextColor,
theme.disabledOpacity
)}
circleStyle={{
height: scale(22),
marginHorizontal: scale(3),
width: scale(22)
}}
height={scale(28)}
onSyncPress={(on: boolean): void => {
if (on) {
handleChangeNotif('weekly');
} else {
handleChangeNotif('never');
}
}}
style={styles.switch}
value={isSwitchOn}
width={scale(48)}
/>
{isSwitchOn ? (
<ActionPicker
actionSheetOptions={{
cancelButtonIndex: 3,
options: notificationsValues
.filter(f => f !== 'never') // Don't show never in options
.map(f => i18n.t(`home_frequency_${f}`)) // Translate
.map(capitalize)
.concat(i18n.t('home_frequency_cancel'))
}}
callback={(buttonIndex): void => {
if (buttonIndex === 3) {
// 3 is cancel
return;
}

handleChangeNotif(notificationsValues[buttonIndex + 1]); // +1 because we skipped neve
}}
>
<>
<Text style={styles.label}>
{i18n.t('home_frequency_notify_me')}
</Text>
<Text style={styles.labelFrequency}>
{i18n.t(`home_frequency_${notif}`)}{' '}
<FontAwesome name="caret-down" />
</Text>
</>
</ActionPicker>
) : (
<Text style={styles.label}>
{i18n.t('home_frequency_allow_notifications')}
</Text>
)}
</View>
);
}
14 changes: 7 additions & 7 deletions App/Screens/Home/Footer/ShareButton/ShareButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@
// along with Sh**t! I Smoke. If not, see <http://www.gnu.org/licenses/>.

import React, { createRef, useContext } from 'react';
import { Share, StyleSheet, View } from 'react-native';
import { Share, StyleSheet, View, ViewProps } from 'react-native';
import { captureRef } from 'react-native-view-shot';

import { Button } from '../../../../components';
import { CircleButton } from '../../../../components';
import { i18n } from '../../../../localization';
import { ApiContext, CurrentLocationContext } from '../../../../stores';
import { ShareImage } from './ShareImage';

type ShareButtonProps = ViewProps;

const styles = StyleSheet.create({
viewShot: {
// We don't want to show this on the screen. If you have a better idea how
Expand All @@ -33,7 +35,7 @@ const styles = StyleSheet.create({
}
});

export function ShareButton(): React.ReactElement {
export function ShareButton(props: ShareButtonProps): React.ReactElement {
const { api } = useContext(ApiContext);
const { currentLocation } = useContext(CurrentLocationContext);
const refViewShot = createRef<View>();
Expand Down Expand Up @@ -71,13 +73,11 @@ export function ShareButton(): React.ReactElement {
}

return (
<View>
<View {...props}>
<View collapsable={false} ref={refViewShot} style={styles.viewShot}>
<ShareImage />
</View>
<Button icon="share-alt" onPress={handleShare} type="secondary">
{i18n.t('home_btn_share').toUpperCase()}
</Button>
<CircleButton icon="ios-share-alt" onPress={handleShare} />
</View>
);
}
6 changes: 1 addition & 5 deletions App/Screens/Home/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
marginTop: theme.spacing.mini
},
distanceText: {
...theme.text,
flex: 1
},
warning: {
marginRight: theme.spacing.mini,
marginTop: scale(-2) // FIXME We shouldn't need that, with `alignItems: 'center'` on .distance
Expand Down Expand Up @@ -93,7 +89,7 @@ export function Header(props: HeaderProps): React.ReactElement {
/>
<View style={styles.distance}>
{isTooFar && <Image source={alert} style={styles.warning} />}
<Text style={styles.distanceText}>
<Text style={theme.text}>
{i18n.t('home_header_air_quality_station_distance', {
distanceToStation: distance,
distanceUnit: shortDistanceUnit
Expand Down

0 comments on commit fc9bffc

Please sign in to comment.