Verified Commit f5f87ec0 authored by Bram's avatar Bram Committed by Sébastiaan Versteeg
Browse files

Add basic profile picture updating functionality

parent a22ab52d
......@@ -79,6 +79,7 @@ dependencies {
implementation project(':@react-native-community_status-bar')
implementation project(':@react-native-community_async-storage')
implementation project(':react-native-device-info')
implementation project(':react-native-image-picker')
implementation project(':react-native-screens')
implementation project(':react-native-gesture-handler')
implementation project(':react-native-sentry')
......
......@@ -4,10 +4,11 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission tools:node="remove" android:name="android.permission.READ_PHONE_STATE" />
<uses-permission tools:node="remove" android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission tools:node="remove" android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:name=".MainApplication"
......
......@@ -8,6 +8,7 @@ import com.reactnativecommunity.asyncstorage.AsyncStoragePackage;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
import com.swmansion.rnscreens.RNScreensPackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
import com.imagepicker.ImagePickerPackage;
import cl.json.ShareApplication;
import cl.json.RNSharePackage;
......@@ -44,6 +45,7 @@ public class MainApplication extends Application implements ShareApplication, Re
new RNDeviceInfo(),
new RNScreensPackage(),
new RNGestureHandlerPackage(),
new ImagePickerPackage(),
new RNSharePackage(),
new RNSentryPackage(),
new SnackbarPackage(),
......
......@@ -9,6 +9,8 @@ include ':react-native-screens'
project(':react-native-screens').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-screens/android')
include ':react-native-gesture-handler'
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
include ':react-native-image-picker'
project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android')
include ':react-native-share'
project(':react-native-share').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-share/android')
include ':react-native-sentry'
......
......@@ -2,6 +2,9 @@ export const PROFILE = 'PROFILE_PROFILE';
export const FETCHING = 'PROFILE_FETCHING';
export const SUCCESS = 'PROFILE_SUCCESS';
export const FAILURE = 'PROFILE_FAILURE';
export const UPDATING = 'PROFILE_UPDATING';
export const UPDATE_SUCCESS = 'PROFILE_UPDATE_SUCCESS';
export const UPDATE_FAIL = 'PROFILE_UPDATE_FAIL';
export function profile(member = 'me') {
return {
......@@ -16,6 +19,25 @@ export function fetching() {
};
}
export function updating() {
return {
type: UPDATING,
};
}
export function updateSuccess(profileData) {
return {
type: UPDATE_SUCCESS,
payload: { profileData },
};
}
export function updateFail() {
return {
type: UPDATE_FAIL,
};
}
export function success(profileData) {
return {
type: SUCCESS,
......
......@@ -30,6 +30,6 @@ export function fetchUserInfo() {
return { type: FETCH_USER_INFO };
}
export function setUserInfo(displayName, photo) {
return { type: SET_USER_INFO, payload: { displayName, photo } };
export function setUserInfo(displayName, photo, pk) {
return { type: SET_USER_INFO, payload: { displayName, photo, pk } };
}
......@@ -24,6 +24,7 @@ const initialState = {
},
success: false,
hasLoaded: false,
updating: false,
};
export default function profile(state = initialState, action = {}) {
......@@ -46,6 +47,23 @@ export default function profile(state = initialState, action = {}) {
success: false,
hasLoaded: true,
};
case profileActions.UPDATING:
return {
...state,
updating: true,
};
case profileActions.UPDATE_SUCCESS:
return {
...state,
updating: false,
success: true,
};
case profileActions.UPDATE_FAIL:
return {
...state,
updating: false,
success: true,
};
default:
return state;
}
......
......@@ -11,6 +11,7 @@ const initialState = {
token: '',
username: '',
displayName: '',
pk: -1,
photo: defaultProfileImage,
};
......@@ -33,6 +34,7 @@ export default function session(state = initialState, action = {}) {
...state,
displayName: action.payload.displayName,
photo: action.payload.photo,
pk: action.payload.pk,
};
case sessionActions.TOKEN_INVALID:
case sessionActions.SIGN_OUT:
......
......@@ -7,7 +7,7 @@ import { apiRequest } from '../utils/url';
import * as profileActions from '../actions/profile';
import { tokenSelector } from '../selectors/session';
const profile = function* profile(action) {
function* profile(action) {
const { member } = action.payload;
const token = yield select(tokenSelector);
......@@ -29,10 +29,32 @@ const profile = function* profile(action) {
Sentry.captureException(error);
yield put(profileActions.failure());
}
};
}
const profileSaga = function* eventSaga() {
function* uploadProfilePicture(action) {
const { member } = action.payload;
const token = yield select(tokenSelector);
yield put(profileActions.updating());
const data = {
method: 'PUT',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Token ${token}`,
};
}
try {
const profileData = yield call(apiRequest, `members/${member}`, data);
yield put(profileActions.updateSuccess(profileData));
} catch (error) {
Sentry.captureException(error);
yield put(profileActions.updateFail());
}
}
function* eventSaga() {
yield takeEvery(profileActions.PROFILE, profile);
};
}
export default profileSaga;
......@@ -128,7 +128,7 @@ function* userInfo() {
[DISPLAYNAMEKEY, userProfile.display_name],
[PHOTOKEY, userProfile.avatar.medium],
]);
yield put(sessionActions.setUserInfo(userProfile.display_name, userProfile.avatar.medium));
yield put(sessionActions.setUserInfo(userProfile.display_name, userProfile.avatar.medium, userProfile.pk));
} catch (error) {
Sentry.captureException(error);
}
......
......@@ -6,12 +6,19 @@ import {
Platform,
ScrollView,
TouchableOpacity,
TouchableWithoutFeedback,
View,
Image,
Modal,
} from 'react-native';
import StatusBar from '@react-native-community/status-bar';
import { withTranslation } from 'react-i18next';
import LinearGradient from 'react-native-linear-gradient';
import Icon from 'react-native-vector-icons/MaterialIcons';
import Moment from 'moment';
import ImagePicker from 'react-native-image-picker';
import StandardHeader from '../../components/standardHeader/StandardHeader';
import LoadingScreen from '../../components/loadingScreen/LoadingScreen';
import ErrorScreen from '../../components/errorScreen/ErrorScreen';
import LoadingScreen from '../../components/loadingScreen/LoadingScreen';
......@@ -29,14 +36,27 @@ import styles, {
HEADER_SCROLL_DISTANCE,
} from './style/Profile';
class ProfileScreen extends Component {
constructor(props) {
super(props);
this.scrollY = new Animated.Value(0);
this.textHeight = Platform.OS === 'android' ? 27 : 22;
this.state = {
modalVisible: false,
image: this.props.profile.avatar.full,
};
}
setModalVisible = (visible) => {
this.setState({
modalVisible: visible,
});
};
isOwnProfilePage = () => {
return this.props.pk === this.props.profile.pk;
};
getAppbar = () => {
const headerHeight = this.scrollY.interpolate({
inputRange: [0, HEADER_SCROLL_DISTANCE],
......@@ -76,8 +96,7 @@ class ProfileScreen extends Component {
fontSize: textSize,
bottom: textPosBottom,
};
let appBarBorderStyle = {
};
let appBarBorderStyle = {};
if (Platform.OS === 'android') {
textStyle = {
...textStyle,
......@@ -101,44 +120,75 @@ class ProfileScreen extends Component {
}
return (
<Animated.View style={[styles.header, { height: headerHeight }]}>
<Animated.View
style={[
styles.backgroundImage,
{
opacity: imageOpacity,
transform: [{ translateY: imageTranslate }],
},
]}
>
<ImageBackground
source={{ uri: this.props.profile.avatar.full }}
style={styles.backgroundImage}
resizeMode="cover"
>
<LinearGradient colors={['#55000000', '#000000']} style={styles.overlayGradient} />
</ImageBackground>
</Animated.View>
<Animated.View style={[styles.appBar, appBarBorderStyle]}>
<TouchableOpacity
onPress={this.props.goBack}
<TouchableWithoutFeedback
onPress={() => this.setModalVisible(true)}
>
<Animated.View style={[styles.header, { height: headerHeight }]}>
<Animated.View
style={[
styles.backgroundImage,
{
opacity: imageOpacity,
transform: [{ translateY: imageTranslate }],
},
]}
>
<Icon
name="arrow-back"
style={styles.icon}
size={24}
/>
</TouchableOpacity>
<Animated.Text
style={[styles.title, textStyle]}
>
{this.props.profile.display_name}
</Animated.Text>
<ImageBackground
source={{ uri: this.state.image }}
style={styles.backgroundImage}
resizeMode="cover"
>
<LinearGradient colors={['#55000000', '#000000']} style={styles.overlayGradient}/>
</ImageBackground>
</Animated.View>
<Animated.View style={[styles.appBar, appBarBorderStyle]}>
<TouchableOpacity
onPress={this.props.goBack}
>
<Icon
name="arrow-back"
style={styles.icon}
size={24}
/>
</TouchableOpacity>
<Animated.Text
style={[styles.title, textStyle]}
>
{this.props.profile.display_name}
</Animated.Text>
</Animated.View>
</Animated.View>
</Animated.View>
</TouchableWithoutFeedback>
);
};
handleEditPress = () => {
const options = {
title: 'Select Avatar',
storageOptions: {
skipBackup: true,
path: 'images',
},
};
ImagePicker.launchImageLibrary(options, (response) => {
console.log('Response = ', response);
if (response.didCancel) {
console.log('User cancelled image picker');
} else if (response.error) {
console.log('ImagePicker Error: ', response.error);
} else if (response.customButton) {
console.log('User tapped custom button: ', response.customButton);
} else {
const source = response.uri;
this.setState({
image: source,
});
}
});
};
render() {
const {
hasLoaded, profile, t, openUrl, success,
......@@ -147,8 +197,8 @@ class ProfileScreen extends Component {
if (!hasLoaded) {
return (
<View style={styles.container}>
<StandardHeader />
<LoadingScreen />
<StandardHeader/>
<LoadingScreen/>
</View>
);
} if (!success) {
......@@ -168,6 +218,44 @@ class ProfileScreen extends Component {
translucent
animated
/>
<Modal
visible={this.state.modalVisible}
transparent={true}
onRequestClose={() => this.setModalVisible(false)}
animationType={'fade'}
>
<View
style={{
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(52, 52, 52, 0.8)',
width: '100%',
height: '100%'
}}
>
<Image
source={{ uri: this.state.image }}
style={{ width: '90%', height: '60%' }}
resizeMode={'contain'}
/>
<View
style={{ justifyContent: 'space-around', flexDirection: 'row' }}
>
{this.isOwnProfilePage() && (
<Icon
name="edit"
style={{ flex: 1, fontSize: 24, color: 'white', textAlign: 'center' }}
onPress={this.handleEditPress}
/>
)}
<Icon
name="cancel"
style={{ flex: 1, fontSize: 24, color: 'white', textAlign: 'center' }}
onPress={() => this.setModalVisible(false)}
/>
</View>
</View>
</Modal>
<ScrollView
style={styles.container}
scrollEventThrottle={16}
......@@ -187,6 +275,7 @@ class ProfileScreen extends Component {
}
ProfileScreen.propTypes = {
pk: PropTypes.number.isRequired,
profile: PropTypes.shape({
pk: PropTypes.number.isRequired,
display_name: PropTypes.string.isRequired,
......
......@@ -6,6 +6,7 @@ const mapStateToProps = state => ({
profile: state.profile.profile,
success: state.profile.success,
hasLoaded: state.profile.hasLoaded,
pk: state.session.pk,
});
const mapDispatchToProps = {
......
......@@ -9,6 +9,8 @@ target 'ThaliApp' do
pod 'Firebase/Core', '~> 5.20.1'
pod 'Firebase/Messaging', '~> 5.20.1'
pod 'react-native-image-picker', :path => '../node_modules/react-native-image-picker'
target 'ThaliAppTests' do
inherit! :search_paths
# Pods for testing
......
......@@ -80,5 +80,13 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSPhotoLibraryUsageDescription</key>
<string>$(PRODUCT_NAME) would like access to your photo gallery</string>
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) would like to use your camera</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>$(PRODUCT_NAME) would like to save photos to your photo gallery</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) would like to your microphone (for videos)</string>
</dict>
</plist>
......@@ -1197,7 +1197,15 @@ absolute-path@^0.0.0:
resolved "https://registry.yarnpkg.com/absolute-path/-/absolute-path-0.0.0.tgz#a78762fbdadfb5297be99b15d35a785b2f095bf7"
integrity sha1-p4di+9rftSl76ZsV01p4Wy8JW/c=
accepts@~1.3.5, accepts@~1.3.7:
accepts@~1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2"
integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I=
dependencies:
mime-types "~2.1.18"
negotiator "0.6.1"
accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
......@@ -1228,12 +1236,11 @@ acorn-export-ns-from@^0.1.0:
integrity sha512-QDQJBe2DfxNBIMxs+19XY2i/XXilJn+kPgX30HWNYK4IXoNj3ACNSWPU7szL0SzqjFyOG4zoZxG9P7JfNw5g7A==
acorn-globals@^4.1.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.2.tgz#4e2c2313a597fd589720395f6354b41cd5ec8006"
integrity sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ==
version "4.1.0"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.1.0.tgz#ab716025dbe17c54d3ef81d32ece2b2d99fe2538"
integrity sha512-KjZwU26uG3u6eZcfGbTULzFcsoz6pegNKtHPksZPOUsiKo5bUmiBPa38FuHZ/Eun+XYh/JCCkS9AS3Lu4McQOQ==
dependencies:
acorn "^6.0.1"
acorn-walk "^6.0.1"
acorn "^5.0.0"
acorn-import-meta@^1.0.0:
version "1.0.0"
......@@ -1285,9 +1292,9 @@ acorn-walk@^6.0.1, acorn-walk@^6.1.1:
integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==
acorn@^5.5.3:
version "5.7.3"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
version "5.7.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8"
integrity sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==
acorn@^6.0.1, acorn@^6.0.7, acorn@^6.1.1:
version "6.1.1"
......@@ -1330,7 +1337,12 @@ ansi-escapes@^1.1.0:
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
integrity sha1-06ioOzGapneTZisT52HHkRQiMG4=
ansi-escapes@^3.0.0, ansi-escapes@^3.2.0:
ansi-escapes@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30"
integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==
ansi-escapes@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
......@@ -1567,11 +1579,11 @@ async-limiter@~1.0.0:
integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==
async@^2.4.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381"
integrity sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==
version "2.6.1"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==
dependencies:
lodash "^4.17.11"
lodash "^4.17.10"
asynckit@^0.4.0:
version "0.4.0"
......@@ -1579,9 +1591,9 @@ asynckit@^0.4.0:
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
atob@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
version "2.1.1"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.1.tgz#ae2d5a729477f289d60dd7f96a6314a22dd6c22a"
integrity sha1-ri1acpR38onWDdf5amMUoi3Wwio=
aws-sign2@~0.7.0:
version "0.7.0"
......@@ -2088,11 +2100,11 @@ base@^0.11.1:
pascalcase "^0.1.1"
basic-auth@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a"
integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==
version "2.0.0"
resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.0.tgz#015db3f353e02e56377755f962742e8981e7bbba"
integrity sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=
dependencies:
safe-buffer "5.1.2"
safe-buffer "5.1.1"
bcrypt-pbkdf@^1.0.0:
version "1.0.2"
......@@ -2102,9 +2114,9 @@ bcrypt-pbkdf@^1.0.0:
tweetnacl "^0.14.3"
big-integer@^1.6.7:
version "1.6.43"
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.43.tgz#8ac15bf13e93e509500859061233e19d8d0d99d1"
integrity sha512-9dULc9jsKmXl0Aeunug8wbF+58n+hQoFjqClN7WeZwGLh0XJUWyJJ9Ee+Ep+Ql/J9fRsTVaeThp8MhiCCrY0Jg==
version "1.6.34"
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.34.tgz#701affc8f0d73c490930a6b482dc23ed6ffc7484"
integrity sha512-+w6B0Uo0ZvTSzDkXjoBCTNK0oe+aVL+yPi7kwGZm8hd8+Nj1AFPoxoq1Bl/mEu/G/ivOkUc1LRqVR0XeWFUzuA==
bplist-creator@0.0.7:
version "0.0.7"
......@@ -2154,9 +2166,9 @@ braces@^2.3.1:
to-regex "^3.0.1"
browser-process-hrtime@^0.1.2:
version "0.1.3"
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4"
integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==
version "0.1.2"
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz#425d68a58d3447f02a04aa894187fce8af8b7b8e"
integrity sha1-Ql1opY00R/AqBKqJQYf86K+Le44=
browser-resolve@^1.11.3:
version "1.11.3"
......@@ -2304,7 +2316,16 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2:
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==
dependencies:
ansi-styles "^3.2.1"
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
......@@ -2400,11 +2421,10 @@ clone-buffer@^1.0.0:
integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg=
clone-deep@^4.0.0:
version "4.0.1"