Merge remote-tracking branch 'origin/develop' into dbkr/change_is

This commit is contained in:
David Baker 2019-08-13 12:56:58 +01:00
commit e5f913bc11
12 changed files with 82 additions and 129 deletions

View file

@ -148,7 +148,7 @@
"karma-summary-reporter": "^1.5.1", "karma-summary-reporter": "^1.5.1",
"karma-webpack": "^4.0.0-beta.0", "karma-webpack": "^4.0.0-beta.0",
"matrix-mock-request": "^1.2.3", "matrix-mock-request": "^1.2.3",
"matrix-react-test-utils": "^0.1.1", "matrix-react-test-utils": "^0.2.2",
"mocha": "^5.0.5", "mocha": "^5.0.5",
"react-addons-test-utils": "^15.4.0", "react-addons-test-utils": "^15.4.0",
"require-json": "0.0.1", "require-json": "0.0.1",

View file

@ -274,7 +274,7 @@ class ModalManager {
this._reRender(); this._reRender();
return { return {
close: closeDialog, close: closeDialog,
then: (resolve, reject) => onFinishedProm.then(resolve, reject), finished: onFinishedProm,
}; };
} }
@ -285,7 +285,7 @@ class ModalManager {
this._reRender(); this._reRender();
return { return {
close: closeDialog, close: closeDialog,
then: (resolve, reject) => onFinishedProm.then(resolve, reject), finished: onFinishedProm,
}; };
} }

View file

@ -935,7 +935,7 @@ export default React.createClass({
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog'); const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog); const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog);
const [shouldCreate, name, noFederate] = await modal; const [shouldCreate, name, noFederate] = await modal.finished;
if (shouldCreate) { if (shouldCreate) {
const createOpts = {}; const createOpts = {};
if (name) createOpts.name = name; if (name) createOpts.name = name;

View file

@ -15,13 +15,13 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import * as ServerType from '../../views/auth/ServerTypeSelector'; import * as ServerType from '../../views/auth/ServerTypeSelector';
import ServerConfig from "./ServerConfig";
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication'; const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
@ -33,49 +33,8 @@ const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_
* This is a variant of ServerConfig with only the HS field and different body * This is a variant of ServerConfig with only the HS field and different body
* text that is specific to the Modular case. * text that is specific to the Modular case.
*/ */
export default class ModularServerConfig extends React.PureComponent { export default class ModularServerConfig extends ServerConfig {
static propTypes = { static propTypes = ServerConfig.propTypes;
onServerConfigChange: PropTypes.func,
// The current configuration that the user is expecting to change.
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
// Called after the component calls onServerConfigChange
onAfterSubmit: PropTypes.func,
// Optional text for the submit button. If falsey, no button will be shown.
submitText: PropTypes.string,
// Optional class for the submit button. Only applies if the submit button
// is to be rendered.
submitClass: PropTypes.string,
};
static defaultProps = {
onServerConfigChange: function() {},
customHsUrl: "",
delayTimeMs: 0,
};
constructor(props) {
super(props);
this.state = {
busy: false,
errorText: "",
hsUrl: props.serverConfig.hsUrl,
isUrl: props.serverConfig.isUrl,
};
}
componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
newProps.serverConfig.isUrl === this.state.isUrl) return;
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
}
async validateAndApplyServer(hsUrl, isUrl) { async validateAndApplyServer(hsUrl, isUrl) {
// Always try and use the defaults first // Always try and use the defaults first
@ -120,35 +79,6 @@ export default class ModularServerConfig extends React.PureComponent {
return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl); return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl);
} }
onHomeserverBlur = (ev) => {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
this.validateServer();
});
};
onHomeserverChange = (ev) => {
const hsUrl = ev.target.value;
this.setState({ hsUrl });
};
onSubmit = async (ev) => {
ev.preventDefault();
ev.stopPropagation();
const result = await this.validateServer();
if (!result) return; // Do not continue.
if (this.props.onAfterSubmit) {
this.props.onAfterSubmit();
}
};
_waitThenInvoke(existingTimeoutId, fn) {
if (existingTimeoutId) {
clearTimeout(existingTimeoutId);
}
return setTimeout(fn.bind(this), this.props.delayTimeMs);
}
render() { render() {
const Field = sdk.getComponent('elements.Field'); const Field = sdk.getComponent('elements.Field');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');

View file

@ -2,6 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd Copyright 2018, 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -69,10 +70,10 @@ module.exports = React.createClass({
fieldValid: {}, fieldValid: {},
// The ISO2 country code selected in the phone number entry // The ISO2 country code selected in the phone number entry
phoneCountry: this.props.defaultPhoneCountry, phoneCountry: this.props.defaultPhoneCountry,
username: "", username: this.props.defaultUsername || "",
email: "", email: this.props.defaultEmail || "",
phoneNumber: "", phoneNumber: this.props.defaultPhoneNumber || "",
password: "", password: this.props.defaultPassword || "",
passwordConfirm: "", passwordConfirm: "",
passwordComplexity: null, passwordComplexity: null,
passwordSafe: false, passwordSafe: false,
@ -90,7 +91,7 @@ module.exports = React.createClass({
} }
const self = this; const self = this;
if (this.state.email == '') { if (this.state.email === '') {
const haveIs = Boolean(this.props.serverConfig.isUrl); const haveIs = Boolean(this.props.serverConfig.isUrl);
let desc; let desc;
@ -455,7 +456,6 @@ module.exports = React.createClass({
ref={field => this[FIELD_EMAIL] = field} ref={field => this[FIELD_EMAIL] = field}
type="text" type="text"
label={emailPlaceholder} label={emailPlaceholder}
defaultValue={this.props.defaultEmail}
value={this.state.email} value={this.state.email}
onChange={this.onEmailChange} onChange={this.onEmailChange}
onValidate={this.onEmailValidate} onValidate={this.onEmailValidate}
@ -469,7 +469,6 @@ module.exports = React.createClass({
ref={field => this[FIELD_PASSWORD] = field} ref={field => this[FIELD_PASSWORD] = field}
type="password" type="password"
label={_t("Password")} label={_t("Password")}
defaultValue={this.props.defaultPassword}
value={this.state.password} value={this.state.password}
onChange={this.onPasswordChange} onChange={this.onPasswordChange}
onValidate={this.onPasswordValidate} onValidate={this.onPasswordValidate}
@ -483,7 +482,6 @@ module.exports = React.createClass({
ref={field => this[FIELD_PASSWORD_CONFIRM] = field} ref={field => this[FIELD_PASSWORD_CONFIRM] = field}
type="password" type="password"
label={_t("Confirm")} label={_t("Confirm")}
defaultValue={this.props.defaultPassword}
value={this.state.passwordConfirm} value={this.state.passwordConfirm}
onChange={this.onPasswordConfirmChange} onChange={this.onPasswordConfirmChange}
onValidate={this.onPasswordConfirmValidate} onValidate={this.onPasswordConfirmValidate}
@ -512,7 +510,6 @@ module.exports = React.createClass({
ref={field => this[FIELD_PHONE_NUMBER] = field} ref={field => this[FIELD_PHONE_NUMBER] = field}
type="text" type="text"
label={phoneLabel} label={phoneLabel}
defaultValue={this.props.defaultPhoneNumber}
value={this.state.phoneNumber} value={this.state.phoneNumber}
prefix={phoneCountry} prefix={phoneCountry}
onChange={this.onPhoneNumberChange} onChange={this.onPhoneNumberChange}
@ -528,7 +525,6 @@ module.exports = React.createClass({
type="text" type="text"
autoFocus={true} autoFocus={true}
label={_t("Username")} label={_t("Username")}
defaultValue={this.props.defaultUsername}
value={this.state.username} value={this.state.username}
onChange={this.onUsernameChange} onChange={this.onUsernameChange}
onValidate={this.onUsernameValidate} onValidate={this.onUsernameValidate}

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -19,7 +20,6 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk'; import { MatrixClient } from 'matrix-js-sdk';
import AvatarLogic from '../../../Avatar'; import AvatarLogic from '../../../Avatar';
import sdk from '../../../index';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
@ -121,6 +121,10 @@ module.exports = React.createClass({
); );
urls.push(defaultImageUrl); // lowest priority urls.push(defaultImageUrl); // lowest priority
} }
// deduplicate URLs
urls = Array.from(new Set(urls));
return { return {
imageUrls: urls, imageUrls: urls,
defaultImageUrl: defaultImageUrl, defaultImageUrl: defaultImageUrl,

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -17,7 +18,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import { ContentRepo } from 'matrix-js-sdk';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
@ -31,12 +31,21 @@ module.exports = React.createClass({
mxEvent: PropTypes.object.isRequired, mxEvent: PropTypes.object.isRequired,
}, },
onAvatarClick: function(name) { onAvatarClick: function() {
const httpUrl = MatrixClientPeg.get().mxcUrlToHttp(this.props.mxEvent.getContent().url); const cli = MatrixClientPeg.get();
const ev = this.props.mxEvent;
const httpUrl = cli.mxcUrlToHttp(ev.getContent().url);
const room = cli.getRoom(this.props.mxEvent.getRoomId());
const text = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
senderDisplayName: ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(),
roomName: room ? room.name : '',
});
const ImageView = sdk.getComponent("elements.ImageView"); const ImageView = sdk.getComponent("elements.ImageView");
const params = { const params = {
src: httpUrl, src: httpUrl,
name: name, name: text,
}; };
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
}, },
@ -44,29 +53,22 @@ module.exports = React.createClass({
render: function() { render: function() {
const ev = this.props.mxEvent; const ev = this.props.mxEvent;
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const name = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
senderDisplayName: senderDisplayName,
roomName: room ? room.name : '',
});
if (!ev.getContent().url || ev.getContent().url.trim().length === 0) { if (!ev.getContent().url || ev.getContent().url.trim().length === 0) {
return ( return (
<div className="mx_TextualEvent"> <div className="mx_TextualEvent">
{ _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName: senderDisplayName}) } { _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName}) }
</div> </div>
); );
} }
const url = ContentRepo.getHttpUriForMxc( const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
MatrixClientPeg.get().getHomeserverUrl(), // Provide all arguments to RoomAvatar via oobData because the avatar is historic
ev.getContent().url, const oobData = {
Math.ceil(14 * window.devicePixelRatio), avatarUrl: ev.getContent().url,
Math.ceil(14 * window.devicePixelRatio), name: room ? room.name : "",
'crop', };
);
return ( return (
<div className="mx_RoomAvatarEvent"> <div className="mx_RoomAvatarEvent">
@ -75,8 +77,8 @@ module.exports = React.createClass({
{ {
'img': () => 'img': () =>
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar" <AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
onClick={this.onAvatarClick.bind(this, name)}> onClick={this.onAvatarClick}>
<BaseAvatar width={14} height={14} url={url} name={name} /> <RoomAvatar width={14} height={14} oobData={oobData} />
</AccessibleButton>, </AccessibleButton>,
}) })
} }

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -26,6 +27,7 @@ import LanguageDropdown from "../../../elements/LanguageDropdown";
import AccessibleButton from "../../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import {THEMES} from "../../../../../themes";
import PlatformPeg from "../../../../../PlatformPeg"; import PlatformPeg from "../../../../../PlatformPeg";
import MatrixClientPeg from "../../../../../MatrixClientPeg"; import MatrixClientPeg from "../../../../../MatrixClientPeg";
import sdk from "../../../../.."; import sdk from "../../../../..";
@ -160,8 +162,9 @@ export default class GeneralUserSettingsTab extends React.Component {
<span className="mx_SettingsTab_subheading">{_t("Theme")}</span> <span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
<Field id="theme" label={_t("Theme")} element="select" <Field id="theme" label={_t("Theme")} element="select"
value={this.state.theme} onChange={this._onThemeChange}> value={this.state.theme} onChange={this._onThemeChange}>
<option value="light">{_t("Light theme")}</option> {Object.entries(THEMES).map(([theme, text]) => {
<option value="dark">{_t("Dark theme")}</option> return <option key={theme} value={theme}>{_t(text)}</option>;
})}
</Field> </Field>
<SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} /> <SettingsFlag name="useCompactLayout" level={SettingLevel.ACCOUNT} />
</div> </div>

View file

@ -251,6 +251,8 @@
"%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s", "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s",
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s", "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
"Light theme": "Light theme",
"Dark theme": "Dark theme",
"%(displayName)s is typing …": "%(displayName)s is typing …", "%(displayName)s is typing …": "%(displayName)s is typing …",
"%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …",
"%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …",
@ -557,8 +559,6 @@
"Set a new account password...": "Set a new account password...", "Set a new account password...": "Set a new account password...",
"Language and region": "Language and region", "Language and region": "Language and region",
"Theme": "Theme", "Theme": "Theme",
"Light theme": "Light theme",
"Dark theme": "Dark theme",
"Account management": "Account management", "Account management": "Account management",
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
"Deactivate Account": "Deactivate Account", "Deactivate Account": "Deactivate Account",

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,17 +16,13 @@ limitations under the License.
*/ */
import SettingController from "./SettingController"; import SettingController from "./SettingController";
import {DEFAULT_THEME, THEMES} from "../../themes";
const SUPPORTED_THEMES = [
"light",
"dark",
];
export default class ThemeController extends SettingController { export default class ThemeController extends SettingController {
getValueOverride(level, roomId, calculatedValue, calculatedAtLevel) { getValueOverride(level, roomId, calculatedValue, calculatedAtLevel) {
// Override in case some no longer supported theme is stored here // Override in case some no longer supported theme is stored here
if (!SUPPORTED_THEMES.includes(calculatedValue)) { if (!THEMES[calculatedValue]) {
return "light"; return DEFAULT_THEME;
} }
return null; // no override return null; // no override

24
src/themes.js Normal file
View file

@ -0,0 +1,24 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {_td} from "./languageHandler";
export const DEFAULT_THEME = "light";
export const THEMES = {
"light": _td("Light theme"),
"dark": _td("Dark theme"),
};

View file

@ -5061,13 +5061,10 @@ matrix-mock-request@^1.2.3:
bluebird "^3.5.0" bluebird "^3.5.0"
expect "^1.20.2" expect "^1.20.2"
matrix-react-test-utils@^0.1.1: matrix-react-test-utils@^0.2.2:
version "0.1.1" version "0.2.2"
resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.1.1.tgz#b548844d0ebe338ea1b9c8f16474c30d17c3bdf4" resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
integrity sha1-tUiETQ6+M46hucjxZHTDDRfDvfQ= integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
dependencies:
react "^15.6.1"
react-dom "^15.6.1"
md5.js@^1.3.4: md5.js@^1.3.4:
version "1.3.5" version "1.3.5"
@ -6373,7 +6370,7 @@ react-beautiful-dnd@^4.0.1:
redux-thunk "^2.2.0" redux-thunk "^2.2.0"
reselect "^3.0.1" reselect "^3.0.1"
react-dom@^15.6.0, react-dom@^15.6.1: react-dom@^15.6.0:
version "15.6.2" version "15.6.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730"
integrity sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA= integrity sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=
@ -6436,7 +6433,7 @@ react-redux@^5.0.6:
react-is "^16.6.0" react-is "^16.6.0"
react-lifecycles-compat "^3.0.0" react-lifecycles-compat "^3.0.0"
react@^15.6.0, react@^15.6.1: react@^15.6.0:
version "15.6.2" version "15.6.2"
resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72" resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"
integrity sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI= integrity sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=