Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into esc-mark-as-read

This commit is contained in:
Michael Telatynski 2020-04-14 15:37:18 +01:00
commit 73e5f133fb
343 changed files with 6889 additions and 3142 deletions

View file

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,93 +17,10 @@ limitations under the License.
import React from "react";
// derived from code from github.com/noeldelgado/gemini-scrollbar
// Copyright (c) Noel Delgado <pixelia.me@gmail.com> (pixelia.me)
function getScrollbarWidth(alternativeOverflow) {
const div = document.createElement('div');
div.className = 'mx_AutoHideScrollbar'; //to get width of css scrollbar
div.style.position = 'absolute';
div.style.top = '-9999px';
div.style.width = '100px';
div.style.height = '100px';
div.style.overflow = "scroll";
if (alternativeOverflow) {
div.style.overflow = alternativeOverflow;
}
div.style.msOverflowStyle = '-ms-autohiding-scrollbar';
document.body.appendChild(div);
const scrollbarWidth = (div.offsetWidth - div.clientWidth);
document.body.removeChild(div);
return scrollbarWidth;
}
function install() {
const scrollbarWidth = getScrollbarWidth();
if (scrollbarWidth !== 0) {
const hasForcedOverlayScrollbar = getScrollbarWidth('overlay') === 0;
// overflow: overlay on webkit doesn't auto hide the scrollbar
if (hasForcedOverlayScrollbar) {
document.body.classList.add("mx_scrollbar_overlay_noautohide");
} else {
document.body.classList.add("mx_scrollbar_nooverlay");
const style = document.createElement('style');
style.type = 'text/css';
style.innerText =
`body.mx_scrollbar_nooverlay { --scrollbar-width: ${scrollbarWidth}px; }`;
document.head.appendChild(style);
}
}
}
const installBodyClassesIfNeeded = (function() {
let installed = false;
return function() {
if (!installed) {
install();
installed = true;
}
};
})();
export default class AutoHideScrollbar extends React.Component {
constructor(props) {
super(props);
this.onOverflow = this.onOverflow.bind(this);
this.onUnderflow = this.onUnderflow.bind(this);
this._collectContainerRef = this._collectContainerRef.bind(this);
this._needsOverflowListener = null;
}
onOverflow() {
this.containerRef.classList.add("mx_AutoHideScrollbar_overflow");
this.containerRef.classList.remove("mx_AutoHideScrollbar_underflow");
}
onUnderflow() {
this.containerRef.classList.remove("mx_AutoHideScrollbar_overflow");
this.containerRef.classList.add("mx_AutoHideScrollbar_underflow");
}
checkOverflow() {
if (!this._needsOverflowListener) {
return;
}
if (this.containerRef.scrollHeight > this.containerRef.clientHeight) {
this.onOverflow();
} else {
this.onUnderflow();
}
}
componentDidUpdate() {
this.checkOverflow();
}
componentDidMount() {
installBodyClassesIfNeeded();
this._needsOverflowListener =
document.body.classList.contains("mx_scrollbar_nooverlay");
this.checkOverflow();
}
_collectContainerRef(ref) {
@ -126,9 +44,7 @@ export default class AutoHideScrollbar extends React.Component {
onScroll={this.props.onScroll}
onWheel={this.props.onWheel}
>
<div className="mx_AutoHideScrollbar_offset">
{ this.props.children }
</div>
{ this.props.children }
</div>);
}
}

View file

@ -30,7 +30,7 @@ class CustomRoomTagPanel extends React.Component {
};
}
componentWillMount() {
componentDidMount() {
this._tagStoreToken = CustomRoomTagStore.addListener(() => {
this.setState({tags: CustomRoomTagStore.getSortedTags()});
});

View file

@ -37,6 +37,8 @@ export default class EmbeddedPage extends React.PureComponent {
className: PropTypes.string,
// Whether to wrap the page in a scrollbar
scrollbar: PropTypes.bool,
// Map of keys to replace with values, e.g {$placeholder: "value"}
replaceMap: PropTypes.object,
};
static contextType = MatrixClientContext;
@ -56,7 +58,7 @@ export default class EmbeddedPage extends React.PureComponent {
return sanitizeHtml(_t(s));
}
componentWillMount() {
componentDidMount() {
this._unmounted = false;
if (!this.props.url) {
@ -81,6 +83,13 @@ export default class EmbeddedPage extends React.PureComponent {
}
body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1));
if (this.props.replaceMap) {
Object.keys(this.props.replaceMap).forEach(key => {
body = body.split(key).join(this.props.replaceMap[key]);
});
}
this.setState({ page: body });
},
);

View file

@ -428,12 +428,11 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this._unmounted = false;
this._matrixClient = MatrixClientPeg.get();
this._matrixClient.on("Group.myMembership", this._onGroupMyMembership);
this._changeAvatarComponent = null;
this._initGroupStore(this.props.groupId, true);
this._dispatcherRef = dis.register(this._onAction);
@ -451,8 +450,9 @@ export default createReactClass({
}
},
componentWillReceiveProps: function(newProps) {
if (this.props.groupId != newProps.groupId) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(newProps) {
if (this.props.groupId !== newProps.groupId) {
this.setState({
summary: null,
error: null,

View file

@ -0,0 +1,66 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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 * as React from "react";
import AutoHideScrollbar from './AutoHideScrollbar';
import { getHomePageUrl } from "../../utils/pages";
import { _t } from "../../languageHandler";
import SdkConfig from "../../SdkConfig";
import * as sdk from "../../index";
import dis from "../../dispatcher";
const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'});
const onClickExplore = () => dis.dispatch({action: 'view_room_directory'});
const onClickNewRoom = () => dis.dispatch({action: 'view_create_room'});
const HomePage = () => {
const config = SdkConfig.get();
const pageUrl = getHomePageUrl(config);
if (pageUrl) {
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
return <EmbeddedPage className="mx_HomePage" url={pageUrl} scrollbar={true} />;
}
const brandingConfig = config.branding;
let logoUrl = "themes/riot/img/logos/riot-logo.svg";
if (brandingConfig && brandingConfig.authHeaderLogoUrl) {
logoUrl = brandingConfig.authHeaderLogoUrl;
}
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
return <AutoHideScrollbar className="mx_HomePage mx_HomePage_default">
<div className="mx_HomePage_default_wrapper">
<img src={logoUrl} alt="Riot" />
<h1>{ _t("Welcome to %(appName)s", { appName: config.brand || "Riot" }) }</h1>
<h4>{ _t("Liberate your communication") }</h4>
<div className="mx_HomePage_default_buttons">
<AccessibleButton onClick={onClickSendDm} className="mx_HomePage_button_sendDm">
{ _t("Send a Direct Message") }
</AccessibleButton>
<AccessibleButton onClick={onClickExplore} className="mx_HomePage_button_explore">
{ _t("Explore Public Rooms") }
</AccessibleButton>
<AccessibleButton onClick={onClickNewRoom} className="mx_HomePage_button_createGroup">
{ _t("Create a Group Chat") }
</AccessibleButton>
</div>
</div>
</AutoHideScrollbar>;
};
export default HomePage;

View file

@ -66,6 +66,22 @@ export default class IndicatorScrollbar extends React.Component {
this._autoHideScrollbar = autoHideScrollbar;
}
componentDidUpdate(prevProps) {
const prevLen = prevProps && prevProps.children && prevProps.children.length || 0;
const curLen = this.props.children && this.props.children.length || 0;
// check overflow only if amount of children changes.
// if we don't guard here, we end up with an infinite
// render > componentDidUpdate > checkOverflow > setState > render loop
if (prevLen !== curLen) {
this.checkOverflow();
}
}
componentDidMount() {
this.checkOverflow();
}
checkOverflow() {
const hasTopOverflow = this._scrollElement.scrollTop > 0;
const hasBottomOverflow = this._scrollElement.scrollHeight >
@ -95,10 +111,6 @@ export default class IndicatorScrollbar extends React.Component {
this._scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow");
}
if (this._autoHideScrollbar) {
this._autoHideScrollbar.checkOverflow();
}
if (this.props.trackHorizontalOverflow) {
this.setState({
// Offset from absolute position of the container

View file

@ -1,6 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -24,6 +24,8 @@ import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryCom
import * as sdk from '../../index';
export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
export default createReactClass({
displayName: 'InteractiveAuth',
@ -47,7 +49,7 @@ export default createReactClass({
// @param {bool} status True if the operation requiring
// auth was completed sucessfully, false if canceled.
// @param {object} result The result of the authenticated call
// if successful, otherwise the error object
// if successful, otherwise the error object.
// @param {object} extra Additional information about the UI Auth
// process:
// * emailSid {string} If email auth was performed, the sid of
@ -75,6 +77,15 @@ export default createReactClass({
// is managed by some other party and should not be managed by
// the component itself.
continueIsManaged: PropTypes.bool,
// Called when the stage changes, or the stage's phase changes. First
// argument is the stage, second is the phase. Some stages do not have
// phases and will be counted as 0 (numeric).
onStagePhaseChange: PropTypes.func,
// continueText and continueKind are passed straight through to the AuthEntryComponent.
continueText: PropTypes.string,
continueKind: PropTypes.string,
},
getInitialState: function() {
@ -87,7 +98,8 @@ export default createReactClass({
};
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._unmounted = false;
this._authLogic = new InteractiveAuth({
authData: this.props.authData,
@ -204,6 +216,16 @@ export default createReactClass({
this._authLogic.submitAuthDict(authData);
},
_onPhaseChange: function(newPhase) {
if (this.props.onStagePhaseChange) {
this.props.onStagePhaseChange(this.state.authStage, newPhase || 0);
}
},
_onStageCancel: function() {
this.props.onAuthFinished(false, ERROR_USER_CANCELLED);
},
_renderCurrentStage: function() {
const stage = this.state.authStage;
if (!stage) {
@ -232,6 +254,10 @@ export default createReactClass({
fail={this._onAuthStageFailed}
setEmailSid={this._setEmailSid}
showContinue={!this.props.continueIsManaged}
onPhaseChange={this._onPhaseChange}
continueText={this.props.continueText}
continueKind={this.props.continueKind}
onCancel={this._onStageCancel}
/>
);
},

View file

@ -44,7 +44,8 @@ const LeftPanel = createReactClass({
};
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this.focusedElement = null;
this._breadcrumbsWatcherRef = SettingsStore.watchSetting(

View file

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 New Vector Ltd
Copyright 2017, 2018, 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,23 +16,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from 'matrix-js-sdk';
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { DragDropContext } from 'react-beautiful-dnd';
import { Key, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard';
import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler';
import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index';
import dis from '../../dispatcher';
import sessionStore from '../../stores/SessionStore';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore";
import RoomListStore from "../../stores/RoomListStore";
import { getHomePageUrl } from '../../utils/pages';
import TagOrderActions from '../../actions/TagOrderActions';
import RoomListActions from '../../actions/RoomListActions';
@ -40,6 +39,8 @@ import ResizeHandle from '../views/elements/ResizeHandle';
import {Resizer, CollapseDistributor} from '../../resizer';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
import HomePage from "./HomePage";
import ResizeNotifier from "../../utils/ResizeNotifier";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
// NB. this is just for server notices rather than pinned messages in general.
@ -52,6 +53,52 @@ function canElementReceiveInput(el) {
!!el.getAttribute("contenteditable");
}
interface IProps {
matrixClient: MatrixClient;
onRegistered: (credentials: MatrixClientCreds) => Promise<MatrixClient>;
viaServers?: string[];
hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier;
middleDisabled: boolean;
initialEventPixelOffset: number;
leftDisabled: boolean;
rightDisabled: boolean;
showCookieBar: boolean;
hasNewVersion: boolean;
userHasGeneratedPassword: boolean;
showNotifierToolbar: boolean;
page_type: string;
autoJoin: boolean;
thirdPartyInvite?: object;
roomOobData?: object;
currentRoomId: string;
ConferenceHandler?: object;
collapseLhs: boolean;
checkingForUpdate: boolean;
config: {
piwik: {
policyUrl: string;
},
[key: string]: any,
};
currentUserId?: string;
currentGroupId?: string;
currentGroupIsNew?: boolean;
version?: string;
newVersion?: string;
newVersionReleaseNotes?: string;
}
interface IState {
mouseDown?: {
x: number;
y: number;
};
syncErrorData: any;
useCompactLayout: boolean;
serverNoticeEvents: MatrixEvent[];
userHasGeneratedPassword: boolean;
}
/**
* This is what our MatrixChat shows when we are logged in. The precise view is
* determined by the page_type property.
@ -61,10 +108,10 @@ function canElementReceiveInput(el) {
*
* Components mounted below us can access the matrix client via the react context.
*/
const LoggedInView = createReactClass({
displayName: 'LoggedInView',
class LoggedInView extends React.PureComponent<IProps, IState> {
static displayName = 'LoggedInView';
propTypes: {
static propTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
page_type: PropTypes.string.isRequired,
onRoomCreated: PropTypes.func,
@ -77,24 +124,28 @@ const LoggedInView = createReactClass({
viaServers: PropTypes.arrayOf(PropTypes.string),
// and lots and lots of other stuff.
},
};
getInitialState: function() {
return {
protected readonly _matrixClient: MatrixClient;
protected readonly _roomView: React.RefObject<any>;
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
protected readonly _sessionStore: sessionStore;
protected readonly _sessionStoreToken: { remove: () => void };
protected resizer: Resizer;
constructor(props, context) {
super(props, context);
this.state = {
mouseDown: undefined,
syncErrorData: undefined,
userHasGeneratedPassword: false,
// use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
// any currently active server notice events
serverNoticeEvents: [],
};
},
componentDidMount: function() {
this.resizer = this._createResizer();
this.resizer.attach();
this._loadResizerPreferences();
},
componentWillMount: function() {
// stash the MatrixClient in case we log out before we are unmounted
this._matrixClient = this.props.matrixClient;
@ -116,22 +167,29 @@ const LoggedInView = createReactClass({
fixupColorFonts();
this._roomView = createRef();
},
this._roomView = React.createRef();
this._resizeContainer = React.createRef();
}
componentDidUpdate(prevProps) {
componentDidMount() {
this.resizer = this._createResizer();
this.resizer.attach();
this._loadResizerPreferences();
}
componentDidUpdate(prevProps, prevState) {
// attempt to guess when a banner was opened or closed
if (
(prevProps.showCookieBar !== this.props.showCookieBar) ||
(prevProps.hasNewVersion !== this.props.hasNewVersion) ||
(prevProps.userHasGeneratedPassword !== this.props.userHasGeneratedPassword) ||
(prevState.userHasGeneratedPassword !== this.state.userHasGeneratedPassword) ||
(prevProps.showNotifierToolbar !== this.props.showNotifierToolbar)
) {
this.props.resizeNotifier.notifyBannersChanged();
}
},
}
componentWillUnmount: function() {
componentWillUnmount() {
document.removeEventListener('keydown', this._onNativeKeyDown, false);
this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync);
@ -140,7 +198,7 @@ const LoggedInView = createReactClass({
this._sessionStoreToken.remove();
}
this.resizer.detach();
},
}
// Child components assume that the client peg will not be null, so give them some
// sort of assurance here by only allowing a re-render if the client is truthy.
@ -148,22 +206,22 @@ const LoggedInView = createReactClass({
// This is required because `LoggedInView` maintains its own state and if this state
// updates after the client peg has been made null (during logout), then it will
// attempt to re-render and the children will throw errors.
shouldComponentUpdate: function() {
shouldComponentUpdate() {
return Boolean(MatrixClientPeg.get());
},
}
canResetTimelineInRoom: function(roomId) {
canResetTimelineInRoom = (roomId) => {
if (!this._roomView.current) {
return true;
}
return this._roomView.current.canResetTimeline();
},
};
_setStateFromSessionStore() {
_setStateFromSessionStore = () => {
this.setState({
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
});
},
};
_createResizer() {
const classNames = {
@ -187,24 +245,22 @@ const LoggedInView = createReactClass({
},
};
const resizer = new Resizer(
this.resizeContainer,
this._resizeContainer.current,
CollapseDistributor,
collapseConfig);
resizer.setClassNames(classNames);
return resizer;
},
}
_loadResizerPreferences() {
let lhsSize = window.localStorage.getItem("mx_lhs_size");
if (lhsSize !== null) {
lhsSize = parseInt(lhsSize, 10);
} else {
let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10);
if (isNaN(lhsSize)) {
lhsSize = 350;
}
this.resizer.forHandleAt(0).resize(lhsSize);
},
}
onAccountData: function(event) {
onAccountData = (event) => {
if (event.getType() === "im.vector.web.settings") {
this.setState({
useCompactLayout: event.getContent().useCompactLayout,
@ -213,9 +269,9 @@ const LoggedInView = createReactClass({
if (event.getType() === "m.ignored_user_list") {
dis.dispatch({action: "ignore_state_changed"});
}
},
};
onSync: function(syncState, oldSyncState, data) {
onSync = (syncState, oldSyncState, data) => {
const oldErrCode = (
this.state.syncErrorData &&
this.state.syncErrorData.error &&
@ -237,16 +293,16 @@ const LoggedInView = createReactClass({
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
this._updateServerNoticeEvents();
}
},
};
onRoomStateEvents: function(ev, state) {
onRoomStateEvents = (ev, state) => {
const roomLists = RoomListStore.getRoomLists();
if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
this._updateServerNoticeEvents();
}
},
};
_updateServerNoticeEvents: async function() {
_updateServerNoticeEvents = async () => {
const roomLists = RoomListStore.getRoomLists();
if (!roomLists['m.server_notice']) return [];
@ -259,16 +315,16 @@ const LoggedInView = createReactClass({
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
const ev = timeline.getEvents().find(ev => ev.getId() === eventId);
if (ev) pinnedEvents.push(ev);
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
if (event) pinnedEvents.push(event);
}
}
this.setState({
serverNoticeEvents: pinnedEvents,
});
},
};
_onPaste: function(ev) {
_onPaste = (ev) => {
let canReceiveInput = false;
let element = ev.target;
// test for all parents because the target can be a child of a contenteditable element
@ -282,7 +338,7 @@ const LoggedInView = createReactClass({
// so dispatch synchronously before paste happens
dis.dispatch({action: 'focus_composer'}, true);
}
},
};
/*
SOME HACKERY BELOW:
@ -306,22 +362,22 @@ const LoggedInView = createReactClass({
We also listen with a native listener on the document to get keydown events when no element is focused.
Bubbling is irrelevant here as the target is the body element.
*/
_onReactKeyDown: function(ev) {
_onReactKeyDown = (ev) => {
// events caught while bubbling up on the root element
// of this component, so something must be focused.
this._onKeyDown(ev);
},
};
_onNativeKeyDown: function(ev) {
_onNativeKeyDown = (ev) => {
// only pass this if there is no focused element.
// if there is, _onKeyDown will be called by the
// react keydown handler that respects the react bubbling order.
if (ev.target === document.body) {
this._onKeyDown(ev);
}
},
};
_onKeyDown: function(ev) {
_onKeyDown = (ev) => {
/*
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
// Will need to find a better meta key if anyone actually cares about using this.
@ -380,7 +436,7 @@ const LoggedInView = createReactClass({
break;
case Key.SLASH:
if (ctrlCmdOnly) {
if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev)) {
KeyboardShortcuts.toggleDialog();
handled = true;
}
@ -431,19 +487,19 @@ const LoggedInView = createReactClass({
// that would prevent typing in the now-focussed composer
}
}
},
};
/**
* dispatch a page-up/page-down/etc to the appropriate component
* @param {Object} ev The key event
*/
_onScrollKeyPressed: function(ev) {
_onScrollKeyPressed = (ev) => {
if (this._roomView.current) {
this._roomView.current.handleScrollKey(ev);
}
},
};
_onDragEnd: function(result) {
_onDragEnd = (result) => {
// Dragged to an invalid destination, not onto a droppable
if (!result.destination) {
return;
@ -466,9 +522,9 @@ const LoggedInView = createReactClass({
} else if (dest.startsWith('room-sub-list-droppable_')) {
this._onRoomTileEndDrag(result);
}
},
};
_onRoomTileEndDrag: function(result) {
_onRoomTileEndDrag = (result) => {
let newTag = result.destination.droppableId.split('_')[1];
let prevTag = result.source.droppableId.split('_')[1];
if (newTag === 'undefined') newTag = undefined;
@ -485,9 +541,9 @@ const LoggedInView = createReactClass({
prevTag, newTag,
oldIndex, newIndex,
), true);
},
};
_onMouseDown: function(ev) {
_onMouseDown = (ev) => {
// When the panels are disabled, clicking on them results in a mouse event
// which bubbles to certain elements in the tree. When this happens, close
// any settings page that is currently open (user/room/group).
@ -506,9 +562,9 @@ const LoggedInView = createReactClass({
});
}
}
},
};
_onMouseUp: function(ev) {
_onMouseUp = (ev) => {
if (!this.state.mouseDown) return;
const deltaX = ev.pageX - this.state.mouseDown.x;
@ -527,17 +583,12 @@ const LoggedInView = createReactClass({
// Always clear the mouseDown state to ensure we don't accidentally
// use stale values due to the mouseDown checks.
this.setState({mouseDown: null});
},
};
_setResizeContainerRef(div) {
this.resizeContainer = div;
},
render: function() {
render() {
const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RoomView = sdk.getComponent('structures.RoomView');
const UserView = sdk.getComponent('structures.UserView');
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
const GroupView = sdk.getComponent('structures.GroupView');
const MyGroups = sdk.getComponent('structures.MyGroups');
const ToastContainer = sdk.getComponent('structures.ToastContainer');
@ -576,13 +627,7 @@ const LoggedInView = createReactClass({
break;
case PageTypes.HomePage:
{
const pageUrl = getHomePageUrl(this.props.config);
pageElement = <EmbeddedPage className="mx_HomePage"
url={pageUrl}
scrollbar={true}
/>;
}
pageElement = <HomePage />;
break;
case PageTypes.UserView:
@ -653,7 +698,7 @@ const LoggedInView = createReactClass({
{ topBar }
<ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._setResizeContainerRef} className={bodyClasses}>
<div ref={this._resizeContainer} className={bodyClasses}>
<LeftPanel
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
@ -666,7 +711,7 @@ const LoggedInView = createReactClass({
</div>
</MatrixClientContext.Provider>
);
},
});
}
}
export default LoggedInView;

View file

@ -221,7 +221,8 @@ export default createReactClass({
return {serverConfig: props};
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
SdkConfig.put(this.props.config);
// Used by _viewRoom before getting state from sync
@ -261,9 +262,7 @@ export default createReactClass({
this._accountPassword = null;
this._accountPasswordTimer = null;
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this._themeWatcher = new ThemeWatcher();
this._themeWatcher.start();
@ -361,7 +360,8 @@ export default createReactClass({
if (this._accountPasswordTimer !== null) clearTimeout(this._accountPasswordTimer);
},
componentWillUpdate: function(props, state) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
UNSAFE_componentWillUpdate: function(props, state) {
if (this.shouldTrackPageChange(this.state, state)) {
this.startPageChangeTimer();
}
@ -382,7 +382,7 @@ export default createReactClass({
// Tor doesn't support performance
if (!performance || !performance.mark) return null;
// This shouldn't happen because componentWillUpdate and componentDidUpdate
// This shouldn't happen because UNSAFE_componentWillUpdate and componentDidUpdate
// are used.
if (this._pageChanging) {
console.warn('MatrixChat.startPageChangeTimer: timer already started');
@ -657,6 +657,7 @@ export default createReactClass({
collapseLhs: true,
});
break;
case 'focus_room_filter': // for CtrlOrCmd+K to work by expanding the left panel first
case 'show_left_panel':
this.setState({
collapseLhs: false,
@ -1520,7 +1521,7 @@ export default createReactClass({
} else if (request.pending) {
ToastStore.sharedInstance().addOrReplaceToast({
key: 'verifreq_' + request.channel.transactionId,
title: _t("Verification Request"),
title: request.isSelfVerification ? _t("Self-verification request") : _t("Verification Request"),
icon: "verification",
props: {request},
component: sdk.getComponent("toasts.VerificationRequestToast"),
@ -1906,13 +1907,19 @@ export default createReactClass({
}
// Test for the master cross-signing key in SSSS as a quick proxy for
// whether cross-signing has been set up on the account.
let masterKeyInStorage = false;
try {
masterKeyInStorage = !!await cli.getAccountDataFromServer("m.cross_signing.master");
} catch (e) {
if (e.errcode !== "M_NOT_FOUND") {
console.warn("Secret storage account data check failed", e);
// whether cross-signing has been set up on the account. We can't
// really continue until we know whether it's there or not so retry
// if this fails.
let masterKeyInStorage;
while (masterKeyInStorage === undefined) {
try {
masterKeyInStorage = !!await cli.getAccountDataFromServer("m.cross_signing.master");
} catch (e) {
if (e.errcode === "M_NOT_FOUND") {
masterKeyInStorage = false;
} else {
console.warn("Secret storage account data check failed: retrying...", e);
}
}
}
@ -2021,7 +2028,7 @@ export default createReactClass({
}
} else if (this.state.view === VIEWS.WELCOME) {
const Welcome = sdk.getComponent('auth.Welcome');
view = <Welcome />;
view = <Welcome {...this.getServerProperties()} />;
} else if (this.state.view === VIEWS.REGISTER) {
const Registration = sdk.getComponent('structures.auth.Registration');
view = (

View file

@ -838,14 +838,16 @@ class CreationGrouper {
// events that we include in the group but then eject out and place
// above the group.
this.ejectedEvents = [];
this.readMarker = panel._readMarkerForEvent(createEvent.getId());
this.readMarker = panel._readMarkerForEvent(
createEvent.getId(),
createEvent === lastShownEvent,
);
}
shouldGroup(ev) {
const panel = this.panel;
const createEvent = this.createEvent;
if (!panel._shouldShowEvent(ev)) {
this.readMarker = this.readMarker || panel._readMarkerForEvent(ev.getId());
return true;
}
if (panel._wantsDateSeparator(this.createEvent, ev.getDate())) {
@ -863,7 +865,10 @@ class CreationGrouper {
add(ev) {
const panel = this.panel;
this.readMarker = this.readMarker || panel._readMarkerForEvent(ev.getId());
this.readMarker = this.readMarker || panel._readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
if (!panel._shouldShowEvent(ev)) {
return;
}
@ -950,7 +955,10 @@ class MemberGrouper {
constructor(panel, ev, prevEvent, lastShownEvent) {
this.panel = panel;
this.readMarker = panel._readMarkerForEvent(ev.getId());
this.readMarker = panel._readMarkerForEvent(
ev.getId(),
ev === lastShownEvent,
);
this.events = [ev];
this.prevEvent = prevEvent;
this.lastShownEvent = lastShownEvent;
@ -971,7 +979,10 @@ class MemberGrouper {
const renderText = textForEvent(ev);
if (!renderText || renderText.trim().length === 0) return; // quietly ignore
}
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(ev.getId());
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
this.events.push(ev);
}

View file

@ -38,7 +38,7 @@ export default createReactClass({
contextType: MatrixClientContext,
},
componentWillMount: function() {
componentDidMount: function() {
this._fetch();
},

View file

@ -108,7 +108,7 @@ export default class RightPanel extends React.Component {
}
}
componentWillMount() {
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
const cli = this.context;
cli.on("RoomState.members", this.onRoomStateMember);
@ -123,7 +123,8 @@ export default class RightPanel extends React.Component {
this._unregisterGroupStore(this.props.groupId);
}
componentWillReceiveProps(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore(this.props.groupId);
this._initGroupStore(newProps.groupId);

View file

@ -56,7 +56,8 @@ export default createReactClass({
};
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this._unmounted = false;
this.nextBatch = null;
this.filterTimeout = null;
@ -89,9 +90,7 @@ export default createReactClass({
),
});
});
},
componentDidMount: function() {
this.refreshRoomList();
},

View file

@ -96,7 +96,7 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);

View file

@ -126,7 +126,7 @@ export default class RoomSubList extends React.PureComponent {
break;
case 'view_room':
if (this.state.hidden && !this.props.forceExpand &&
if (this.state.hidden && !this.props.forceExpand && payload.show_room_tile &&
this.props.list.some((r) => r.roomId === payload.room_id)
) {
this.toggle();
@ -193,6 +193,7 @@ export default class RoomSubList extends React.PureComponent {
onRoomTileClick = (roomId, ev) => {
dis.dispatch({
action: 'view_room',
show_room_tile: true, // to make sure the room gets scrolled into view
room_id: roomId,
clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),
});

View file

@ -55,6 +55,7 @@ import RightPanelStore from "../../stores/RightPanelStore";
import {haveTileForEvent} from "../views/rooms/EventTile";
import RoomContext from "../../contexts/RoomContext";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { shieldStatusForRoom } from '../../utils/ShieldUtils';
const DEBUG = false;
let debuglog = function() {};
@ -167,7 +168,8 @@ export default createReactClass({
};
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this.context.on("Room", this.onRoom);
this.context.on("Room.timeline", this.onRoomTimeline);
@ -180,6 +182,7 @@ export default createReactClass({
this.context.on("crypto.keyBackupStatus", this.onKeyBackupStatus);
this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
// Start listening for RoomViewStore updates
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate);
@ -235,6 +238,11 @@ export default createReactClass({
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
};
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
// Stop peeking because we have joined this room now
this.context.stopPeeking();
}
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
console.log(
'RVS update:',
@ -466,6 +474,10 @@ export default createReactClass({
RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState());
}
if (this.state.shouldPeek) {
this.context.stopPeeking();
}
// stop tracking room changes to format permalinks
this._stopAllPermalinkCreators();
@ -493,6 +505,7 @@ export default createReactClass({
this.context.removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus);
this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
}
window.removeEventListener('beforeunload', this.onPageUnload);
@ -814,6 +827,13 @@ export default createReactClass({
this._updateE2EStatus(room);
},
onCrossSigningKeysChanged: function() {
const room = this.state.room;
if (room) {
this._updateE2EStatus(room);
}
},
_updateE2EStatus: async function(room) {
if (!this.context.isRoomEncrypted(room.roomId)) {
return;
@ -837,40 +857,9 @@ export default createReactClass({
return;
}
// Duplication between here and _updateE2eStatus in RoomTile
/* At this point, the user has encryption on and cross-signing on */
const e2eMembers = await room.getEncryptionTargetMembers();
const verified = [];
const unverified = [];
e2eMembers.map(({userId}) => userId)
.filter((userId) => userId !== this.context.getUserId())
.forEach((userId) => {
(this.context.checkUserTrust(userId).isCrossSigningVerified() ?
verified : unverified).push(userId);
});
debuglog("e2e verified", verified, "unverified", unverified);
/* Check all verified user devices. */
/* Don't alarm if no other users are verified */
const targets = (verified.length > 0) ? [...verified, this.context.getUserId()] : verified;
for (const userId of targets) {
const devices = await this.context.getStoredDevicesForUser(userId);
const anyDeviceNotVerified = devices.some(({deviceId}) => {
return !this.context.checkDeviceTrust(userId, deviceId).isVerified();
});
if (anyDeviceNotVerified) {
this.setState({
e2eStatus: "warning",
});
debuglog("e2e status set to warning as not all users trust all of their sessions." +
" Aborted on user", userId);
return;
}
}
this.setState({
e2eStatus: unverified.length === 0 ? "verified" : "normal",
e2eStatus: await shieldStatusForRoom(this.context, room),
});
},
@ -1243,7 +1232,7 @@ export default createReactClass({
});
}, function(error) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Search failed: " + error);
console.error("Search failed", error);
Modal.createTrackedDialog('Search failed', '', ErrorDialog, {
title: _t("Search failed"),
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or search timed out :(")),

View file

@ -156,9 +156,8 @@ export default createReactClass({
};
},
componentWillMount: function() {
this._fillRequestWhileRunning = false;
this._isFilling = false;
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._pendingFillRequests = {b: null, f: null};
if (this.props.resizeNotifier) {

View file

@ -53,6 +53,7 @@ export default createReactClass({
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._search = createRef();
},

View file

@ -20,6 +20,7 @@ import * as React from "react";
import {_t} from '../../languageHandler';
import * as PropTypes from "prop-types";
import * as sdk from "../../index";
import AutoHideScrollbar from './AutoHideScrollbar';
import { ReactNode } from "react";
/**
@ -113,9 +114,9 @@ export default class TabbedView extends React.Component<IProps, IState> {
private _renderTabPanel(tab: Tab): React.ReactNode {
return (
<div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}>
<div className='mx_TabbedView_tabPanelContent'>
<AutoHideScrollbar className='mx_TabbedView_tabPanelContent'>
{tab.body}
</div>
</AutoHideScrollbar>
</div>
);
}

View file

@ -44,7 +44,7 @@ const TagPanel = createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this.unmounted = false;
this.context.on("Group.myMembership", this._onGroupMyMembership);
this.context.on("sync", this._onClientSync);

View file

@ -202,7 +202,8 @@ const TimelinePanel = createReactClass({
};
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
debuglog("TimelinePanel: mounting");
this.lastRRSentEventId = undefined;
@ -234,7 +235,8 @@ const TimelinePanel = createReactClass({
this._initTimeline(this.props);
},
componentWillReceiveProps: function(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(newProps) {
if (newProps.timelineSet !== this.props.timelineSet) {
// throw new Error("changing timelineSet on a TimelinePanel is not supported");

View file

@ -35,7 +35,7 @@ export default class UserView extends React.Component {
this.state = {};
}
componentWillMount() {
componentDidMount() {
if (this.props.userId) {
this._loadProfileInfo();
}

View file

@ -18,13 +18,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { accessSecretStorage, AccessCancelledError } from '../../../CrossSigningManager';
const PHASE_INTRO = 0;
const PHASE_BUSY = 1;
const PHASE_DONE = 2;
const PHASE_CONFIRM_SKIP = 3;
import {
SetupEncryptionStore,
PHASE_INTRO,
PHASE_BUSY,
PHASE_DONE,
PHASE_CONFIRM_SKIP,
} from '../../../stores/SetupEncryptionStore';
import SetupEncryptionBody from "./SetupEncryptionBody";
export default class CompleteSecurity extends React.Component {
static propTypes = {
@ -33,232 +34,42 @@ export default class CompleteSecurity extends React.Component {
constructor() {
super();
this.state = {
phase: PHASE_INTRO,
// this serves dual purpose as the object for the request logic and
// the presence of it insidicating that we're in 'verify mode'.
// Because of the latter, it lives in the state.
verificationRequest: null,
backupInfo: null,
};
MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest);
const store = SetupEncryptionStore.sharedInstance();
store.on("update", this._onStoreUpdate);
store.start();
this.state = {phase: store.phase};
}
_onStoreUpdate = () => {
const store = SetupEncryptionStore.sharedInstance();
this.setState({phase: store.phase});
};
componentWillUnmount() {
if (this.state.verificationRequest) {
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
}
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest);
}
}
_onUsePassphraseClick = async () => {
this.setState({
phase: PHASE_BUSY,
});
const cli = MatrixClientPeg.get();
try {
const backupInfo = await cli.getKeyBackupVersion();
this.setState({backupInfo});
// The control flow is fairly twisted here...
// For the purposes of completing security, we only wait on getting
// as far as the trust check and then show a green shield.
// We also begin the key backup restore as well, which we're
// awaiting inside `accessSecretStorage` only so that it keeps your
// passphase cached for that work. This dialog itself will only wait
// on the first trust check, and the key backup restore will happen
// in the background.
await new Promise((resolve, reject) => {
try {
accessSecretStorage(async () => {
await cli.checkOwnCrossSigningTrust();
resolve();
if (backupInfo) {
// A complete restore can take many minutes for large
// accounts / slow servers, so we allow the dialog
// to advance before this.
await cli.restoreKeyBackupWithSecretStorage(backupInfo);
}
});
} catch (e) {
console.error(e);
reject(e);
}
});
if (cli.getCrossSigningId()) {
this.setState({
phase: PHASE_DONE,
});
}
} catch (e) {
if (!(e instanceof AccessCancelledError)) {
console.log(e);
}
// this will throw if the user hits cancel, so ignore
this.setState({
phase: PHASE_INTRO,
});
}
}
onVerificationRequest = async (request) => {
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
if (this.state.verificationRequest) {
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
}
await request.accept();
request.on("change", this.onVerificationRequestChange);
this.setState({
verificationRequest: request,
});
}
onVerificationRequestChange = () => {
if (this.state.verificationRequest.cancelled) {
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
this.setState({
verificationRequest: null,
});
}
}
onSkipClick = () => {
this.setState({
phase: PHASE_CONFIRM_SKIP,
});
}
onSkipConfirmClick = () => {
this.props.onFinished();
}
onSkipBackClick = () => {
this.setState({
phase: PHASE_INTRO,
});
}
onDoneClick = () => {
this.props.onFinished();
const store = SetupEncryptionStore.sharedInstance();
store.off("update", this._onStoreUpdate);
store.stop();
}
render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {
phase,
} = this.state;
const {phase} = this.state;
let icon;
let title;
let body;
if (this.state.verificationRequest) {
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
body = <EncryptionPanel
layout="dialog"
verificationRequest={this.state.verificationRequest}
onClose={this.props.onFinished}
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
/>;
} else if (phase === PHASE_INTRO) {
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
if (phase === PHASE_INTRO) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
title = _t("Complete security");
body = (
<div>
<p>{_t(
"Open an existing session & use it to verify this one, " +
"granting it access to encrypted messages.",
)}</p>
<p className="mx_CompleteSecurity_waiting"><InlineSpinner />{_t("Waiting…")}</p>
<p>{_t(
"If you cant access one, <button>use your recovery key or passphrase.</button>",
{}, {
button: sub => <AccessibleButton element="span"
className="mx_linkButton"
onClick={this._onUsePassphraseClick}
>
{sub}
</AccessibleButton>,
})}</p>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton
kind="danger"
onClick={this.onSkipClick}
>
{_t("Skip")}
</AccessibleButton>
</div>
</div>
);
} else if (phase === PHASE_DONE) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified"></span>;
title = _t("Session verified");
let message;
if (this.state.backupInfo) {
message = <p>{_t(
"Your new session is now verified. It has access to your " +
"encrypted messages, and other users will see it as trusted.",
)}</p>;
} else {
message = <p>{_t(
"Your new session is now verified. Other users will see it as trusted.",
)}</p>;
}
body = (
<div>
<div className="mx_CompleteSecurity_heroIcon mx_E2EIcon_verified"></div>
{message}
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton
kind="primary"
onClick={this.onDoneClick}
>
{_t("Done")}
</AccessibleButton>
</div>
</div>
);
} else if (phase === PHASE_CONFIRM_SKIP) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
title = _t("Are you sure?");
body = (
<div>
<p>{_t(
"Without completing security on this session, it wont have " +
"access to encrypted messages.",
)}</p>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton
className="warning"
kind="secondary"
onClick={this.onSkipConfirmClick}
>
{_t("Skip")}
</AccessibleButton>
<AccessibleButton
kind="danger"
onClick={this.onSkipBackClick}
>
{_t("Go Back")}
</AccessibleButton>
</div>
</div>
);
} else if (phase === PHASE_BUSY) {
const Spinner = sdk.getComponent('views.elements.Spinner');
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
title = _t("Complete security");
body = <Spinner />;
} else {
throw new Error(`Unknown phase ${phase}`);
}
@ -271,7 +82,7 @@ export default class CompleteSecurity extends React.Component {
{title}
</h2>
<div className="mx_CompleteSecurity_body">
{body}
<SetupEncryptionBody onFinished={this.props.onFinished} />
</div>
</CompleteSecurityBody>
</AuthPage>

View file

@ -69,12 +69,13 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this.reset = null;
this._checkServerLiveliness(this.props.serverConfig);
},
componentWillReceiveProps: function(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
@ -296,7 +297,6 @@ export default createReactClass({
<form onSubmit={this.onSubmitForm}>
<div className="mx_AuthBody_fieldRow">
<Field
id="mx_ForgotPassword_email"
name="reset_email" // define a name so browser's password autofill gets less confused
type="text"
label={_t('Email')}
@ -307,7 +307,6 @@ export default createReactClass({
</div>
<div className="mx_AuthBody_fieldRow">
<Field
id="mx_ForgotPassword_password"
name="reset_password"
type="password"
label={_t('Password')}
@ -315,7 +314,6 @@ export default createReactClass({
onChange={this.onInputChanged.bind(this, "password")}
/>
<Field
id="mx_ForgotPassword_passwordConfirm"
name="reset_password_confirm"
type="password"
label={_t('Confirm')}

View file

@ -113,7 +113,8 @@ export default createReactClass({
};
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this._unmounted = false;
// map from login step type to a function which will render a control
@ -133,7 +134,8 @@ export default createReactClass({
this._unmounted = true;
},
componentWillReceiveProps(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;

View file

@ -37,7 +37,7 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
// There is some assymetry between ChangeDisplayName and ChangeAvatar,
// as ChangeDisplayName will auto-get the name but ChangeAvatar expects
// the URL to be passed to you (because it's also used for room avatars).

View file

@ -120,12 +120,13 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this._unmounted = false;
this._replaceClient();
},
componentWillReceiveProps(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
@ -462,7 +463,7 @@ export default createReactClass({
initial_device_display_name: this.props.defaultDeviceDisplayName,
};
if (auth) registerParams.auth = auth;
if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibitLogin = inhibitLogin;
if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibit_login = inhibitLogin;
return this.state.matrixClient.registerRequest(registerParams);
},

View file

@ -0,0 +1,196 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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 React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import {
SetupEncryptionStore,
PHASE_INTRO,
PHASE_BUSY,
PHASE_DONE,
PHASE_CONFIRM_SKIP,
PHASE_FINISHED,
} from '../../../stores/SetupEncryptionStore';
export default class SetupEncryptionBody extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
constructor() {
super();
const store = SetupEncryptionStore.sharedInstance();
store.on("update", this._onStoreUpdate);
store.start();
this.state = {
phase: store.phase,
// this serves dual purpose as the object for the request logic and
// the presence of it indicating that we're in 'verify mode'.
// Because of the latter, it lives in the state.
verificationRequest: store.verificationRequest,
backupInfo: store.backupInfo,
};
}
_onStoreUpdate = () => {
const store = SetupEncryptionStore.sharedInstance();
if (store.phase === PHASE_FINISHED) {
this.props.onFinished();
return;
}
this.setState({
phase: store.phase,
verificationRequest: store.verificationRequest,
backupInfo: store.backupInfo,
});
};
componentWillUnmount() {
const store = SetupEncryptionStore.sharedInstance();
store.off("update", this._onStoreUpdate);
store.stop();
}
_onUsePassphraseClick = async () => {
const store = SetupEncryptionStore.sharedInstance();
store.usePassPhrase();
}
onSkipClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.skip();
}
onSkipConfirmClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.skipConfirm();
}
onSkipBackClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.returnAfterSkip();
}
onDoneClick = () => {
const store = SetupEncryptionStore.sharedInstance();
store.done();
}
render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const {
phase,
} = this.state;
if (this.state.verificationRequest) {
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
return <EncryptionPanel
layout="dialog"
verificationRequest={this.state.verificationRequest}
onClose={this.props.onFinished}
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
/>;
} else if (phase === PHASE_INTRO) {
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
return (
<div>
<p>{_t(
"Open an existing session & use it to verify this one, " +
"granting it access to encrypted messages.",
)}</p>
<p className="mx_CompleteSecurity_waiting"><InlineSpinner />{_t("Waiting…")}</p>
<p>{_t(
"If you cant access one, <button>use your recovery key or passphrase.</button>",
{}, {
button: sub => <AccessibleButton element="span"
className="mx_linkButton"
onClick={this._onUsePassphraseClick}
>
{sub}
</AccessibleButton>,
})}</p>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton
kind="danger"
onClick={this.onSkipClick}
>
{_t("Skip")}
</AccessibleButton>
</div>
</div>
);
} else if (phase === PHASE_DONE) {
let message;
if (this.state.backupInfo) {
message = <p>{_t(
"Your new session is now verified. It has access to your " +
"encrypted messages, and other users will see it as trusted.",
)}</p>;
} else {
message = <p>{_t(
"Your new session is now verified. Other users will see it as trusted.",
)}</p>;
}
return (
<div>
<div className="mx_CompleteSecurity_heroIcon mx_E2EIcon_verified"></div>
{message}
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton
kind="primary"
onClick={this.onDoneClick}
>
{_t("Done")}
</AccessibleButton>
</div>
</div>
);
} else if (phase === PHASE_CONFIRM_SKIP) {
return (
<div>
<p>{_t(
"Without completing security on this session, it wont have " +
"access to encrypted messages.",
)}</p>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton
className="warning"
kind="secondary"
onClick={this.onSkipConfirmClick}
>
{_t("Skip")}
</AccessibleButton>
<AccessibleButton
kind="danger"
onClick={this.onSkipBackClick}
>
{_t("Go Back")}
</AccessibleButton>
</div>
</div>
);
} else if (phase === PHASE_BUSY) {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <Spinner />;
} else {
console.log(`SetupEncryptionBody: Unknown phase ${phase}`);
}
}
}

View file

@ -54,7 +54,7 @@ export default class SoftLogout extends React.Component {
this.state = {
loginView: LOGIN_VIEW.LOADING,
keyBackupNeeded: true, // assume we do while we figure it out (see componentWillMount)
keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount)
busy: false,
password: "",
@ -213,7 +213,6 @@ export default class SoftLogout extends React.Component {
<p>{introText}</p>
{error}
<Field
id="softlogout_password"
type="password"
label={_t("Password")}
onChange={this.onPasswordChange}

View file

@ -46,7 +46,8 @@ export default createReactClass({
};
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._captchaWidgetId = null;
this._recaptchaContainer = createRef();

View file

@ -60,7 +60,7 @@ export default class CountryDropdown extends React.Component {
};
}
componentWillMount() {
componentDidMount() {
if (!this.props.value) {
// If no value is given, we start with the default
// country selected, but our parent component

View file

@ -1,7 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -25,6 +25,7 @@ import classnames from 'classnames';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
/* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed
@ -59,11 +60,21 @@ import SettingsStore from "../../../settings/SettingsStore";
* session to be failed and the process to go back to the start.
* setEmailSid: m.login.email.identity only: a function to be called with the
* email sid after a token is requested.
* onPhaseChange: A function which is called when the stage's phase changes. If
* the stage has no phases, call this with DEFAULT_PHASE. Takes
* one argument, the phase, and is always defined/required.
* continueText: For stages which have a continue button, the text to use.
* continueKind: For stages which have a continue button, the style of button to
* use. For example, 'danger' or 'primary'.
* onCancel A function with no arguments which is called by the stage if the
* user knowingly cancelled/dismissed the authentication attempt.
*
* Each component may also provide the following functions (beyond the standard React ones):
* focus: set the input focus appropriately in the form.
*/
export const DEFAULT_PHASE = 0;
export const PasswordAuthEntry = createReactClass({
displayName: 'PasswordAuthEntry',
@ -78,6 +89,11 @@ export const PasswordAuthEntry = createReactClass({
// is the auth logic currently waiting for something to
// happen?
busy: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
},
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
getInitialState: function() {
@ -145,7 +161,6 @@ export const PasswordAuthEntry = createReactClass({
<p>{ _t("Confirm your identity by entering your account password below.") }</p>
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
<Field
id="mx_InteractiveAuthEntryComponents_password"
className={passwordBoxClass}
type="password"
name="passwordField"
@ -176,6 +191,11 @@ export const RecaptchaAuthEntry = createReactClass({
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
},
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
_onCaptchaResponse: function(response) {
@ -237,8 +257,14 @@ export const TermsAuthEntry = createReactClass({
errorText: PropTypes.string,
busy: PropTypes.bool,
showContinue: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
},
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
// TODO: [REACT-WARNING] Move this to constructor
componentWillMount: function() {
// example stageParams:
//
@ -379,6 +405,11 @@ export const EmailIdentityAuthEntry = createReactClass({
stageState: PropTypes.object.isRequired,
fail: PropTypes.func.isRequired,
setEmailSid: PropTypes.func.isRequired,
onPhaseChange: PropTypes.func.isRequired,
},
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
getInitialState: function() {
@ -421,6 +452,7 @@ export const MsisdnAuthEntry = createReactClass({
clientSecret: PropTypes.func,
submitAuthDict: PropTypes.func.isRequired,
matrixClient: PropTypes.object,
onPhaseChange: PropTypes.func.isRequired,
},
getInitialState: function() {
@ -430,7 +462,9 @@ export const MsisdnAuthEntry = createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
this._submitUrl = null;
this._sid = null;
this._msisdn = null;
@ -565,6 +599,91 @@ export const MsisdnAuthEntry = createReactClass({
},
});
export class SSOAuthEntry extends React.Component {
static propTypes = {
matrixClient: PropTypes.object.isRequired,
authSessionId: PropTypes.string.isRequired,
loginType: PropTypes.string.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
onPhaseChange: PropTypes.func.isRequired,
continueText: PropTypes.string,
continueKind: PropTypes.string,
onCancel: PropTypes.func,
};
static LOGIN_TYPE = "m.login.sso";
static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso";
static PHASE_PREAUTH = 1; // button to start SSO
static PHASE_POSTAUTH = 2; // button to confirm SSO completed
_ssoUrl: string;
constructor(props) {
super(props);
// We actually send the user through fallback auth so we don't have to
// deal with a redirect back to us, losing application context.
this._ssoUrl = props.matrixClient.getFallbackAuthUrl(
this.props.loginType,
this.props.authSessionId,
);
this.state = {
phase: SSOAuthEntry.PHASE_PREAUTH,
};
}
componentDidMount(): void {
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
}
onStartAuthClick = () => {
// Note: We don't use PlatformPeg's startSsoAuth functions because we almost
// certainly will need to open the thing in a new tab to avoid losing application
// context.
window.open(this._ssoUrl, '_blank');
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
};
onConfirmClick = () => {
this.props.submitAuthDict({});
};
render() {
let continueButton = null;
const cancelButton = (
<AccessibleButton
onClick={this.props.onCancel}
kind={this.props.continueKind ? (this.props.continueKind + '_outline') : 'primary_outline'}
>{_t("Cancel")}</AccessibleButton>
);
if (this.state.phase === SSOAuthEntry.PHASE_PREAUTH) {
continueButton = (
<AccessibleButton
onClick={this.onStartAuthClick}
kind={this.props.continueKind || 'primary'}
>{this.props.continueText || _t("Single Sign On")}</AccessibleButton>
);
} else {
continueButton = (
<AccessibleButton
onClick={this.onConfirmClick}
kind={this.props.continueKind || 'primary'}
>{this.props.continueText || _t("Confirm")}</AccessibleButton>
);
}
return <div className='mx_InteractiveAuthEntryComponents_sso_buttons'>
{cancelButton}
{continueButton}
</div>;
}
}
export const FallbackAuthEntry = createReactClass({
displayName: 'FallbackAuthEntry',
@ -574,9 +693,15 @@ export const FallbackAuthEntry = createReactClass({
loginType: PropTypes.string.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
onPhaseChange: PropTypes.func.isRequired,
},
componentWillMount: function() {
componentDidMount: function() {
this.props.onPhaseChange(DEFAULT_PHASE);
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
// we have to make the user click a button, as browsers will block
// the popup if we open it immediately.
this._popupWindow = null;
@ -598,7 +723,10 @@ export const FallbackAuthEntry = createReactClass({
}
},
_onShowFallbackClick: function() {
_onShowFallbackClick: function(e) {
e.preventDefault();
e.stopPropagation();
const url = this.props.matrixClient.getFallbackAuthUrl(
this.props.loginType,
this.props.authSessionId,
@ -627,7 +755,7 @@ export const FallbackAuthEntry = createReactClass({
}
return (
<div>
<a ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
<a href="" ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
{errorSection}
</div>
);
@ -640,11 +768,12 @@ const AuthEntryComponents = [
EmailIdentityAuthEntry,
MsisdnAuthEntry,
TermsAuthEntry,
SSOAuthEntry,
];
export default function getEntryComponentForLoginType(loginType) {
for (const c of AuthEntryComponents) {
if (c.LOGIN_TYPE == loginType) {
if (c.LOGIN_TYPE === loginType || c.UNSTABLE_LOGIN_TYPE === loginType) {
return c;
}
}

View file

@ -106,7 +106,8 @@ export default class ModularServerConfig extends ServerConfig {
)}
<form onSubmit={this.onSubmit} autoComplete="off" action={null}>
<div className="mx_ServerConfig_fields">
<Field id="mx_ServerConfig_hsUrl"
<Field
id="mx_ServerConfig_hsUrl"
label={_t("Server Name")}
placeholder={this.props.serverConfig.hsUrl}
value={this.state.hsUrl}

View file

@ -193,7 +193,6 @@ export default class PasswordLogin extends React.Component {
classes.error = this.props.loginIncorrect && !this.state.username;
return <Field
className={classNames(classes)}
id="mx_PasswordLogin_email"
name="username" // make it a little easier for browser's remember-password
key="email_input"
type="text"
@ -209,7 +208,6 @@ export default class PasswordLogin extends React.Component {
classes.error = this.props.loginIncorrect && !this.state.username;
return <Field
className={classNames(classes)}
id="mx_PasswordLogin_username"
name="username" // make it a little easier for browser's remember-password
key="username_input"
type="text"
@ -233,7 +231,6 @@ export default class PasswordLogin extends React.Component {
return <Field
className={classNames(classes)}
id="mx_PasswordLogin_phoneNumber"
name="phoneNumber"
key="phone_input"
type="text"
@ -290,7 +287,6 @@ export default class PasswordLogin extends React.Component {
<div className="mx_Login_type_container">
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
<Field
id="mx_PasswordLogin_type"
element="select"
value={this.state.loginType}
onChange={this.onLoginTypeChange}
@ -328,7 +324,6 @@ export default class PasswordLogin extends React.Component {
{loginField}
<Field
className={pwFieldClass}
id="mx_PasswordLogin_password"
type="password"
name="password"
label={_t('Password')}

View file

@ -470,7 +470,6 @@ export default createReactClass({
_t("Email") :
_t("Email (optional)");
return <Field
id="mx_RegistrationForm_email"
ref={field => this[FIELD_EMAIL] = field}
type="text"
label={emailPlaceholder}
@ -524,7 +523,6 @@ export default createReactClass({
onOptionChange={this.onPhoneCountryChange}
/>;
return <Field
id="mx_RegistrationForm_phoneNumber"
ref={field => this[FIELD_PHONE_NUMBER] = field}
type="text"
label={phoneLabel}

View file

@ -72,7 +72,8 @@ export default class ServerConfig extends React.PureComponent {
};
}
componentWillReceiveProps(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
newProps.serverConfig.isUrl === this.state.isUrl) return;
@ -223,7 +224,8 @@ export default class ServerConfig extends React.PureComponent {
{sub}
</a>,
})}
<Field id="mx_ServerConfig_hsUrl"
<Field
id="mx_ServerConfig_hsUrl"
label={_t("Homeserver URL")}
placeholder={this.props.serverConfig.hsUrl}
value={this.state.hsUrl}
@ -246,7 +248,7 @@ export default class ServerConfig extends React.PureComponent {
{sub}
</a>,
})}
<Field id="mx_ServerConfig_isUrl"
<Field
label={_t("Identity Server URL")}
placeholder={this.props.serverConfig.isUrl}
value={this.state.isUrl || ''}

View file

@ -18,6 +18,12 @@ import React from 'react';
import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import AuthPage from "./AuthPage";
import * as Matrix from "matrix-js-sdk";
import {_td} from "../../../languageHandler";
import PlatformPeg from "../../../PlatformPeg";
// translatable strings for Welcome pages
_td("Sign in with SSO");
export default class Welcome extends React.PureComponent {
render() {
@ -33,11 +39,24 @@ export default class Welcome extends React.PureComponent {
pageUrl = 'welcome.html';
}
const {hsUrl, isUrl} = this.props.serverConfig;
const tmpClient = Matrix.createClient({
baseUrl: hsUrl,
idBaseUrl: isUrl,
});
const plaf = PlatformPeg.get();
const callbackUrl = plaf.getSSOCallbackUrl(tmpClient.getHomeserverUrl(), tmpClient.getIdentityServerUrl());
return (
<AuthPage>
<div className="mx_Welcome">
<EmbeddedPage className="mx_WelcomePage"
<EmbeddedPage
className="mx_WelcomePage"
url={pageUrl}
replaceMap={{
"$riot:ssoUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "sso"),
"$riot:casUrl": tmpClient.getSsoLoginUrl(callbackUrl.toString(), "cas"),
}}
/>
<LanguageSelector />
</div>

View file

@ -74,7 +74,8 @@ export default createReactClass({
this.context.removeListener('sync', this.onClientSync);
},
componentWillReceiveProps: function(nextProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(nextProps) {
// work out if we need to call setState (if the image URLs array has changed)
const newState = this._getState(nextProps);
const newImageUrls = newState.imageUrls;

View file

@ -51,7 +51,8 @@ export default createReactClass({
return this._getState(this.props);
},
componentWillReceiveProps: function(nextProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(nextProps) {
this.setState(this._getState(nextProps));
},

View file

@ -49,7 +49,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
this._button = createRef();
}
componentWillMount() {
componentDidMount() {
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
}

View file

@ -63,7 +63,8 @@ export default createReactClass({
}
},
componentWillReceiveProps: function(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(newProps) {
this.setState({
urls: this.getImageUrls(newProps),
});

View file

@ -61,7 +61,7 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions);
this._checkPermissions();
},

View file

@ -82,7 +82,7 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this._unmounted = false;
},

View file

@ -35,7 +35,7 @@ export default class StatusMessageContextMenu extends React.Component {
};
}
componentWillMount() {
componentDidMount() {
const { user } = this.props;
if (!user) {
return;

View file

@ -26,6 +26,7 @@ import { getHostingLink } from '../../../utils/HostingLink';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {MenuItem} from "../../structures/ContextMenu";
import * as sdk from "../../../index";
import {getHomePageUrl} from "../../../utils/pages";
export default class TopLeftMenu extends React.Component {
static propTypes = {
@ -47,15 +48,7 @@ export default class TopLeftMenu extends React.Component {
}
hasHomePage() {
const config = SdkConfig.get();
const pagesConfig = config.embeddedPages;
if (pagesConfig && pagesConfig.homeUrl) {
return true;
}
// This is a deprecated config option for the home page
// (despite the name, given we also now have a welcome
// page, which is not the same).
return !!config.welcomePageUrl;
return !!getHomePageUrl(SdkConfig.get());
}
render() {

View file

@ -107,6 +107,7 @@ export default createReactClass({
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._textinput = createRef();
},

View file

@ -86,7 +86,8 @@ export default createReactClass({
};
},
componentWillMount() {
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount() {
this._matrixClient = MatrixClientPeg.get();
},

View file

@ -166,7 +166,6 @@ export default class BugReportDialog extends React.Component {
) }
</b></p>
<Field
id="mx_BugReportDialog_issueUrl"
type="text"
className="mx_BugReportDialog_field_input"
label={_t("GitHub issue")}
@ -175,7 +174,6 @@ export default class BugReportDialog extends React.Component {
placeholder="https://github.com/vector-im/riot-web/issues/..."
/>
<Field
id="mx_BugReportDialog_notes"
className="mx_BugReportDialog_field_input"
element="textarea"
label={_t("Notes")}

View file

@ -55,7 +55,8 @@ export default createReactClass({
askReason: false,
}),
componentWillMount: function() {
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this._reasonField = null;
},

View file

@ -24,6 +24,7 @@ import withValidation from '../elements/Validation';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {Key} from "../../../Keyboard";
import SettingsStore from "../../../settings/SettingsStore";
export default createReactClass({
displayName: 'CreateRoomDialog',
@ -35,6 +36,7 @@ export default createReactClass({
const config = SdkConfig.get();
return {
isPublic: false,
isEncrypted: true,
name: "",
topic: "",
alias: "",
@ -62,6 +64,11 @@ export default createReactClass({
if (this.state.noFederate) {
createOpts.creation_content = {'m.federate': false};
}
if (!this.state.isPublic && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
opts.encryption = this.state.isEncrypted;
}
return opts;
},
@ -127,6 +134,10 @@ export default createReactClass({
this.setState({isPublic});
},
onEncryptedChange(isEncrypted) {
this.setState({isEncrypted});
},
onAliasChange(alias) {
this.setState({alias});
},
@ -166,19 +177,26 @@ export default createReactClass({
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
let privateLabel;
let publicLabel;
let publicPrivateLabel;
let aliasField;
if (this.state.isPublic) {
publicLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
publicPrivateLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
const domain = MatrixClientPeg.get().getDomain();
aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer">
<RoomAliasField id="alias" ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
<RoomAliasField ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
</div>
);
} else {
privateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
publicPrivateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
}
let e2eeSection;
if (!this.state.isPublic && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
e2eeSection = <React.Fragment>
<LabelledToggleSwitch label={ _t("Enable end-to-end encryption")} onChange={this.onEncryptedChange} value={this.state.isEncrypted} />
<p>{ _t("You cant disable this later. Bridges & most bots wont work yet.") }</p>
</React.Fragment>;
}
const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
@ -188,11 +206,11 @@ export default createReactClass({
>
<form onSubmit={this.onOk} onKeyDown={this._onKeyDown}>
<div className="mx_Dialog_content">
<Field id="name" ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
<Field id="topic" label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} />
<Field ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
<Field label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} className="mx_CreateRoomDialog_topic" />
<LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
{ privateLabel }
{ publicLabel }
{ publicPrivateLabel }
{ e2eeSection }
{ aliasField }
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>

View file

@ -1,6 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -23,71 +23,109 @@ import Analytics from '../../../Analytics';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as Lifecycle from '../../../Lifecycle';
import { _t } from '../../../languageHandler';
import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
continueText: _t("Single Sign On"),
continueKind: "danger",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
body: _t("Are you sure you want to deactivate your account? This is irreversible."),
continueText: _t("Confirm account deactivation"),
continueKind: "danger",
},
};
// This is the same as aestheticsForStagePhases in InteractiveAuthDialog minus the `title`
const DEACTIVATE_AESTHETICS = {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
[PasswordAuthEntry.LOGIN_TYPE]: {
[DEFAULT_PHASE]: {
body: _t("To continue, please enter your password:"),
},
},
};
export default class DeactivateAccountDialog extends React.Component {
constructor(props) {
super(props);
this._onOk = this._onOk.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
this.state = {
password: "",
busy: false,
shouldErase: false,
errStr: null,
authData: null, // for UIA
// A few strings that are passed to InteractiveAuth for design or are displayed
// next to the InteractiveAuth component.
bodyText: null,
continueText: null,
continueKind: null,
};
}
_onPasswordFieldChange(ev) {
this.setState({
password: ev.target.value,
});
}
_onEraseFieldChange(ev) {
this.setState({
shouldErase: ev.target.checked,
});
}
async _onOk() {
this.setState({busy: true});
try {
// This assumes that the HS requires password UI auth
// for this endpoint. In reality it could be any UI auth.
const auth = {
type: 'm.login.password',
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/riot-web/issues/10312
user: MatrixClientPeg.get().credentials.userId,
identifier: {
type: "m.id.user",
user: MatrixClientPeg.get().credentials.userId,
},
password: this.state.password,
};
await MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase);
} catch (err) {
let errStr = _t('Unknown error');
// https://matrix.org/jira/browse/SYN-744
if (err.httpStatus === 401 || err.httpStatus === 403) {
errStr = _t('Incorrect password');
MatrixClientPeg.get().deactivateAccount(null, false).then(r => {
// If we got here, oops. The server didn't require any auth.
// Our application lifecycle will catch the error and do the logout bits.
// We'll try to log something in an vain attempt to record what happened (storage
// is also obliterated on logout).
console.warn("User's account got deactivated without confirmation: Server had no auth");
this.setState({errStr: _t("Server did not require any authentication")});
}).catch(e => {
if (e && e.httpStatus === 401 && e.data) {
// Valid UIA response
this.setState({authData: e.data});
} else {
this.setState({errStr: _t("Server did not return valid authentication information.")});
}
this.setState({
busy: false,
errStr: errStr,
});
});
}
_onStagePhaseChange = (stage, phase) => {
const aesthetics = DEACTIVATE_AESTHETICS[stage];
let bodyText = null;
let continueText = null;
let continueKind = null;
if (aesthetics) {
const phaseAesthetics = aesthetics[phase];
if (phaseAesthetics && phaseAesthetics.body) bodyText = phaseAesthetics.body;
if (phaseAesthetics && phaseAesthetics.continueText) continueText = phaseAesthetics.continueText;
if (phaseAesthetics && phaseAesthetics.continueKind) continueKind = phaseAesthetics.continueKind;
}
this.setState({bodyText, continueText, continueKind});
};
_onUIAuthFinished = (success, result, extra) => {
if (success) return; // great! makeRequest() will be called too.
if (result === ERROR_USER_CANCELLED) {
this._onCancel();
return;
}
Analytics.trackEvent('Account', 'Deactivate Account');
Lifecycle.onLoggedOut();
this.props.onFinished(true);
}
console.error("Error during UI Auth:", {result, extra});
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
};
_onUIAuthComplete = (auth) => {
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
// Deactivation worked - logout & close this dialog
Analytics.trackEvent('Account', 'Deactivate Account');
Lifecycle.onLoggedOut();
this.props.onFinished(true);
}).catch(e => {
console.error(e);
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
});
};
_onEraseFieldChange = (ev) => {
this.setState({
shouldErase: ev.target.checked,
});
};
_onCancel() {
this.props.onFinished(false);
@ -95,34 +133,36 @@ export default class DeactivateAccountDialog extends React.Component {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Loader = sdk.getComponent("elements.Spinner");
let passwordBoxClass = '';
let error = null;
if (this.state.errStr) {
error = <div className="error">
{ this.state.errStr }
</div>;
passwordBoxClass = 'error';
}
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
const okEnabled = this.state.password && !this.state.busy;
let cancelButton = null;
if (!this.state.busy) {
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
{ _t("Cancel") }
</button>;
let auth = <div>{_t("Loading...")}</div>;
if (this.state.authData) {
auth = (
<div>
{this.state.bodyText}
<InteractiveAuth
matrixClient={MatrixClientPeg.get()}
authData={this.state.authData}
makeRequest={this._onUIAuthComplete}
onAuthFinished={this._onUIAuthFinished}
onStagePhaseChange={this._onStagePhaseChange}
continueText={this.state.continueText}
continueKind={this.state.continueKind}
/>
</div>
);
}
const Field = sdk.getComponent('elements.Field');
// this is on purpose not a <form /> to prevent Enter triggering submission, to further prevent accidents
return (
<BaseDialog className="mx_DeactivateAccountDialog"
onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
titleClass="danger"
title={_t("Deactivate Account")}
>
@ -172,29 +212,10 @@ export default class DeactivateAccountDialog extends React.Component {
</label>
</p>
<p>{ _t("To continue, please enter your password:") }</p>
<Field
id="mx_DeactivateAccountDialog_password"
type="password"
label={_t('Password')}
onChange={this._onPasswordFieldChange}
value={this.state.password}
className={passwordBoxClass}
/>
{error}
{auth}
</div>
{ error }
</div>
<div className="mx_Dialog_buttons">
<button
className="mx_Dialog_primary danger"
onClick={this._onOk}
disabled={!okEnabled}
>
{ okLabel }
</button>
{ cancelButton }
</div>
</BaseDialog>
);

View file

@ -279,6 +279,7 @@ export default class DeviceVerifyDialog extends React.Component {
onDone={this._onSasMatchesClick}
isSelf={MatrixClientPeg.get().getUserId() === this.props.userId}
onStartEmoji={this._onUseSasClick}
inDialog={true}
/>;
}

View file

@ -267,7 +267,8 @@ class FilteredList extends React.PureComponent {
};
}
componentWillReceiveProps(nextProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
this.setState({
filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),
@ -302,7 +303,7 @@ class FilteredList extends React.PureComponent {
render() {
const TruncatedList = sdk.getComponent("elements.TruncatedList");
return <div>
<Field id="DevtoolsDialog_FilteredList_filter" label={_t('Filter results')} autoFocus={true} size={64}
<Field label={_t('Filter results')} autoFocus={true} size={64}
type="text" autoComplete="off" value={this.props.query} onChange={this.onQuery}
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
// force re-render so that autoFocus is applied when this component is re-used

View file

@ -196,7 +196,8 @@ export default class IncomingSasDialog extends React.Component {
sas={this._showSasEvent.sas}
onCancel={this._onCancelClick}
onDone={this._onSasMatchesClick}
isSelf={this.props.verifier.userId == MatrixClientPeg.get().getUserId()}
isSelf={this.props.verifier.userId === MatrixClientPeg.get().getUserId()}
inDialog={true}
/>;
}

View file

@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -23,6 +24,7 @@ import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
export default createReactClass({
displayName: 'InteractiveAuthDialog',
@ -44,12 +46,36 @@ export default createReactClass({
onFinished: PropTypes.func.isRequired,
// Optional title and body to show when not showing a particular stage
title: PropTypes.string,
body: PropTypes.string,
// Optional title and body pairs for particular stages and phases within
// those stages. Object structure/example is:
// {
// "org.example.stage_type": {
// 1: {
// "body": "This is a body for phase 1" of org.example.stage_type,
// "title": "Title for phase 1 of org.example.stage_type"
// },
// 2: {
// "body": "This is a body for phase 2 of org.example.stage_type",
// "title": "Title for phase 2 of org.example.stage_type"
// "continueText": "Confirm identity with Example Auth",
// "continueKind": "danger"
// }
// }
// }
aestheticsForStagePhases: PropTypes.object,
},
getInitialState: function() {
return {
authError: null,
// See _onUpdateStagePhase()
uiaStage: null,
uiaStagePhase: null,
};
},
@ -57,12 +83,21 @@ export default createReactClass({
if (success) {
this.props.onFinished(true, result);
} else {
this.setState({
authError: result,
});
if (result === ERROR_USER_CANCELLED) {
this.props.onFinished(false, null);
} else {
this.setState({
authError: result,
});
}
}
},
_onUpdateStagePhase: function(newStage, newPhase) {
// We copy the stage and stage phase params into state for title selection in render()
this.setState({uiaStage: newStage, uiaStagePhase: newPhase});
},
_onDismissClick: function() {
this.props.onFinished(false);
},
@ -71,6 +106,23 @@ export default createReactClass({
const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
// Let's pick a title, body, and other params text that we'll show to the user. The order
// is most specific first, so stagePhase > our props > defaults.
let title = this.state.authError ? 'Error' : (this.props.title || _t('Authentication'));
let body = this.state.authError ? null : this.props.body;
let continueText = null;
let continueKind = null;
if (!this.state.authError && this.props.aestheticsForStagePhases) {
if (this.props.aestheticsForStagePhases[this.state.uiaStage]) {
const aesthetics = this.props.aestheticsForStagePhases[this.state.uiaStage][this.state.uiaStagePhase];
if (aesthetics && aesthetics.title) title = aesthetics.title;
if (aesthetics && aesthetics.body) body = aesthetics.body;
if (aesthetics && aesthetics.continueText) continueText = aesthetics.continueText;
if (aesthetics && aesthetics.continueKind) continueKind = aesthetics.continueKind;
}
}
let content;
if (this.state.authError) {
content = (
@ -88,11 +140,16 @@ export default createReactClass({
} else {
content = (
<div id='mx_Dialog_content'>
<InteractiveAuth ref={this._collectInteractiveAuth}
{body}
<InteractiveAuth
ref={this._collectInteractiveAuth}
matrixClient={this.props.matrixClient}
authData={this.props.authData}
makeRequest={this.props.makeRequest}
onAuthFinished={this._onAuthFinished}
onStagePhaseChange={this._onUpdateStagePhase}
continueText={continueText}
continueKind={continueKind}
/>
</div>
);
@ -101,7 +158,7 @@ export default createReactClass({
return (
<BaseDialog className="mx_InteractiveAuthDialog"
onFinished={this.props.onFinished}
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
title={title}
contentId='mx_Dialog_content'
>
{ content }

View file

@ -123,7 +123,6 @@ export default class ReportEventDialog extends PureComponent {
</p>
{adminMessage}
<Field
id="mx_ReportEventDialog_reason"
className="mx_ReportEventDialog_reason"
element="textarea"
label={_t("Reason")}

View file

@ -36,12 +36,12 @@ export default class RoomSettingsDialog extends React.Component {
onFinished: PropTypes.func.isRequired,
};
componentWillMount() {
componentDidMount() {
this._dispatcherRef = dis.register(this._onAction);
}
componentWillUnmount() {
dis.unregister(this._dispatcherRef);
if (this._dispatcherRef) dis.unregister(this._dispatcherRef);
}
_onAction = (payload) => {
@ -72,7 +72,7 @@ export default class RoomSettingsDialog extends React.Component {
));
tabs.push(new Tab(
_td("Notifications"),
"mx_RoomSettingsDialog_rolesIcon",
"mx_RoomSettingsDialog_notificationsIcon",
<NotificationSettingsTab roomId={this.props.roomId} />,
));

View file

@ -30,7 +30,7 @@ export default createReactClass({
onFinished: PropTypes.func.isRequired,
},
componentWillMount: async function() {
componentDidMount: async function() {
const recommended = await this.props.room.getRecommendedVersion();
this._targetVersion = recommended.version;
this.setState({busy: false});

View file

@ -62,6 +62,7 @@ export default createReactClass({
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._input_value = createRef();
this._uiAuth = createRef();

View file

@ -75,8 +75,8 @@ export default createReactClass({
};
},
componentWillMount: function() {
console.info('SetPasswordDialog component will mount');
componentDidMount: function() {
console.info('SetPasswordDialog component did mount');
},
_onPasswordChanged: function(res) {

View file

@ -0,0 +1,29 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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 SetupEncryptionBody from '../../structures/auth/SetupEncryptionBody';
import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler';
export default function SetupEncryptionDialog({onFinished}) {
return <BaseDialog
headerImage={require("../../../../res/img/e2e/warning.svg")}
onFinished={onFinished}
title={_t("Verify this session")}
>
<SetupEncryptionBody onFinished={onFinished} />
</BaseDialog>;
}

View file

@ -70,9 +70,16 @@ export default class ShareDialog extends React.Component {
this.onCopyClick = this.onCopyClick.bind(this);
this.onLinkSpecificEventCheckboxClick = this.onLinkSpecificEventCheckboxClick.bind(this);
let permalinkCreator: RoomPermalinkCreator = null;
if (props.target instanceof Room) {
permalinkCreator = new RoomPermalinkCreator(props.target);
permalinkCreator.load();
}
this.state = {
// MatrixEvent defaults to share linkSpecificEvent
linkSpecificEvent: this.props.target instanceof MatrixEvent,
permalinkCreator,
};
this._link = createRef();
@ -121,14 +128,6 @@ export default class ShareDialog extends React.Component {
});
}
componentWillMount() {
if (this.props.target instanceof Room) {
const permalinkCreator = new RoomPermalinkCreator(this.props.target);
permalinkCreator.load();
this.setState({permalinkCreator});
}
}
componentWillUnmount() {
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
// the tooltip otherwise, such as pressing Escape or clicking X really quickly

View file

@ -16,14 +16,14 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../languageHandler";
import {CommandCategories, CommandMap} from "../../../SlashCommands";
import {CommandCategories, Commands} from "../../../SlashCommands";
import * as sdk from "../../../index";
export default ({onFinished}) => {
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
const categories = {};
Object.values(CommandMap).forEach(cmd => {
Commands.forEach(cmd => {
if (!categories[cmd.category]) {
categories[cmd.category] = [];
}
@ -41,7 +41,7 @@ export default ({onFinished}) => {
categories[category].forEach(cmd => {
rows.push(<tr key={cmd.command}>
<td><strong>{cmd.command}</strong></td>
<td><strong>{cmd.getCommand()}</strong></td>
<td>{cmd.args}</td>
<td>{cmd.description}</td>
</tr>);

View file

@ -55,6 +55,7 @@ export default createReactClass({
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._field = createRef();
},
@ -116,7 +117,6 @@ export default createReactClass({
</div>
<div>
<Field
id="mx_TextInputDialog_field"
className="mx_TextInputDialog_input"
ref={this._field}
type="text"

View file

@ -87,7 +87,7 @@ export default createReactClass({
onSend: PropTypes.func.isRequired,
},
componentWillMount: function() {
componentDidMount: function() {
MatrixClientPeg.get().on("deviceVerificationChanged", this._onDeviceVerificationChanged);
},

View file

@ -30,16 +30,29 @@ export default class VerificationRequestDialog extends React.Component {
constructor(...args) {
super(...args);
this.onFinished = this.onFinished.bind(this);
this.state = {};
if (this.props.verificationRequest) {
this.state.verificationRequest = this.props.verificationRequest;
} else if (this.props.verificationRequestPromise) {
this.props.verificationRequestPromise.then(r => {
this.setState({verificationRequest: r});
});
}
}
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
const request = this.state.verificationRequest;
const otherUserId = request && request.otherUserId;
const member = this.props.member ||
MatrixClientPeg.get().getUser(this.props.verificationRequest.otherUserId);
otherUserId && MatrixClientPeg.get().getUser(otherUserId);
const title = request && request.isSelfVerification ?
_t("Verify this session") : _t("Verification Request");
return <BaseDialog className="mx_InfoDialog" onFinished={this.onFinished}
contentId="mx_Dialog_content"
title={_t("Verification Request")}
title={title}
hasCancel={true}
>
<EncryptionPanel

View file

@ -200,6 +200,24 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
}
}
async _restoreWithCachedKey(backupInfo) {
if (!backupInfo) return false;
try {
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithCache(
undefined, /* targetRoomId */
undefined, /* targetSessionId */
backupInfo,
);
this.setState({
recoverInfo,
});
return true;
} catch (e) {
console.log("restoreWithCachedKey failed:", e);
return false;
}
}
async _loadBackupStatus() {
this.setState({
loading: true,
@ -213,6 +231,15 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
backupKeyStored,
});
const gotCache = await this._restoreWithCachedKey(backupInfo);
if (gotCache) {
console.log("RestoreKeyBackupDialog: found cached backup key");
this.setState({
loading: false,
});
return;
}
// If the backup key is stored, we can proceed directly to restore.
if (backupKeyStored) {
return this._restoreWithSecretStorage();

View file

@ -46,7 +46,8 @@ export default createReactClass({
};
},
componentWillReceiveProps: function(props) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(props) {
// Make sure the selected item isn't outside the list bounds
const selected = this.state.selected;
const maxSelected = this._maxSelected(props.addressList);

View file

@ -2,6 +2,7 @@
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -41,12 +42,30 @@ import PersistedElement from "./PersistedElement";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
/**
* Does template substitution on a URL (or any string). Variables will be
* passed through encodeURIComponent.
* @param {string} uriTemplate The path with template variables e.g. '/foo/$bar'.
* @param {Object} variables The key/value pairs to replace the template
* variables with. E.g. { '$bar': 'baz' }.
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
*/
function uriFromTemplate(uriTemplate, variables) {
let out = uriTemplate;
for (const [key, val] of Object.entries(variables)) {
out = out.replace(
'$' + key, encodeURIComponent(val),
);
}
return out;
}
export default class AppTile extends React.Component {
constructor(props) {
super(props);
// The key used for PersistedElement
this._persistKey = 'widget_' + this.props.id;
this._persistKey = 'widget_' + this.props.app.id;
this.state = this._getNewState(props);
@ -78,7 +97,7 @@ export default class AppTile extends React.Component {
// This is a function to make the impact of calling SettingsStore slightly less
const hasPermissionToLoad = () => {
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId);
return !!currentlyAllowedWidgets[newProps.eventId];
return !!currentlyAllowedWidgets[newProps.app.eventId];
};
const PersistedElement = sdk.getComponent("elements.PersistedElement");
@ -86,7 +105,7 @@ export default class AppTile extends React.Component {
initialising: true, // True while we are mangling the widget URL
// True while the iframe content is loading
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
widgetUrl: this._addWurlParams(newProps.url),
widgetUrl: this._addWurlParams(newProps.app.url),
// Assume that widget has permission to load if we are the user who
// added it to the room, or if explicitly granted by the user
hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(),
@ -103,7 +122,7 @@ export default class AppTile extends React.Component {
* @return {Boolean} True if capability supported
*/
_hasCapability(capability) {
return ActiveWidgetStore.widgetHasCapability(this.props.id, capability);
return ActiveWidgetStore.widgetHasCapability(this.props.app.id, capability);
}
/**
@ -117,55 +136,52 @@ export default class AppTile extends React.Component {
* If url can not be parsed, it is returned unmodified.
*/
_addWurlParams(urlString) {
const u = url.parse(urlString);
if (!u) {
console.error("_addWurlParams", "Invalid URL", urlString);
return url;
try {
const parsed = new URL(urlString);
// TODO: Replace these with proper widget params
// See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
parsed.searchParams.set('widgetId', this.props.app.id);
parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
// Replace the encoded dollar signs back to dollar signs. They have no special meaning
// in HTTP, but URL parsers encode them anyways.
return parsed.toString().replace(/%24/g, '$');
} catch (e) {
console.error("Failed to add widget URL params:", e);
return urlString;
}
const params = qs.parse(u.query);
// Append widget ID to query parameters
params.widgetId = this.props.id;
// Append current / parent URL, minus the hash because that will change when
// we view a different room (ie. may change for persistent widgets)
params.parentUrl = window.location.href.split('#', 2)[0];
u.search = undefined;
u.query = params;
return u.format();
}
isMixedContent() {
const parentContentProtocol = window.location.protocol;
const u = url.parse(this.props.url);
const u = url.parse(this.props.app.url);
const childContentProtocol = u.protocol;
if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') {
console.warn("Refusing to load mixed-content app:",
parentContentProtocol, childContentProtocol, window.location, this.props.url);
parentContentProtocol, childContentProtocol, window.location, this.props.app.url);
return true;
}
return false;
}
componentWillMount() {
componentDidMount() {
// Only fetch IM token on mount if we're showing and have permission to load
if (this.props.show && this.state.hasPermissionToLoad) {
this.setScalarToken();
}
}
componentDidMount() {
// Widget action listeners
this.dispatcherRef = dis.register(this._onAction);
}
componentWillUnmount() {
// Widget action listeners
dis.unregister(this.dispatcherRef);
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
// if it's not remaining on screen, get rid of the PersistedElement container
if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) {
ActiveWidgetStore.destroyPersistentWidget(this.props.id);
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
const PersistedElement = sdk.getComponent("elements.PersistedElement");
PersistedElement.destroyElement(this._persistKey);
}
@ -176,11 +192,11 @@ export default class AppTile extends React.Component {
* Component initialisation is only complete when this function has resolved
*/
setScalarToken() {
if (!WidgetUtils.isScalarUrl(this.props.url)) {
if (!WidgetUtils.isScalarUrl(this.props.app.url)) {
console.warn('Non-scalar widget, not setting scalar token!', url);
this.setState({
error: null,
widgetUrl: this._addWurlParams(this.props.url),
widgetUrl: this._addWurlParams(this.props.app.url),
initialising: false,
});
return;
@ -191,7 +207,7 @@ export default class AppTile extends React.Component {
console.warn("No integration manager - not setting scalar token", url);
this.setState({
error: null,
widgetUrl: this._addWurlParams(this.props.url),
widgetUrl: this._addWurlParams(this.props.app.url),
initialising: false,
});
return;
@ -204,7 +220,7 @@ export default class AppTile extends React.Component {
console.warn('Non-scalar manager, not setting scalar token!', url);
this.setState({
error: null,
widgetUrl: this._addWurlParams(this.props.url),
widgetUrl: this._addWurlParams(this.props.app.url),
initialising: false,
});
return;
@ -217,7 +233,7 @@ export default class AppTile extends React.Component {
this._scalarClient.getScalarToken().then((token) => {
// Append scalar_token as a query param if not already present
this._scalarClient.scalarToken = token;
const u = url.parse(this._addWurlParams(this.props.url));
const u = url.parse(this._addWurlParams(this.props.app.url));
const params = qs.parse(u.query);
if (!params.scalar_token) {
params.scalar_token = encodeURIComponent(token);
@ -245,14 +261,17 @@ export default class AppTile extends React.Component {
});
}
componentWillReceiveProps(nextProps) {
if (nextProps.url !== this.props.url) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
if (nextProps.app.url !== this.props.app.url) {
this._getNewState(nextProps);
// Fetch IM token for new URL if we're showing and have permission to load
if (this.props.show && this.state.hasPermissionToLoad) {
this.setScalarToken();
}
} else if (nextProps.show && !this.props.show) {
}
if (nextProps.show && !this.props.show) {
// We assume that persisted widgets are loaded and don't need a spinner.
if (this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey)) {
this.setState({
@ -263,7 +282,9 @@ export default class AppTile extends React.Component {
if (this.state.hasPermissionToLoad) {
this.setScalarToken();
}
} else if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
}
if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
this.setState({
widgetPageTitle: nextProps.widgetPageTitle,
});
@ -280,7 +301,7 @@ export default class AppTile extends React.Component {
}
_onEditClick() {
console.log("Edit widget ID ", this.props.id);
console.log("Edit widget ID ", this.props.app.id);
if (this.props.onEditClick) {
this.props.onEditClick();
} else {
@ -289,21 +310,21 @@ export default class AppTile extends React.Component {
IntegrationManagers.sharedInstance().openAll(
this.props.room,
'type_' + this.props.type,
this.props.id,
this.props.app.id,
);
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(
this.props.room,
'type_' + this.props.type,
this.props.id,
this.props.app.id,
);
}
}
}
_onSnapshotClick() {
console.warn("Requesting widget snapshot");
ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot()
console.log("Requesting widget snapshot");
ActiveWidgetStore.getWidgetMessaging(this.props.app.id).getScreenshot()
.catch((err) => {
console.error("Failed to get screenshot", err);
})
@ -315,6 +336,28 @@ export default class AppTile extends React.Component {
});
}
/**
* Ends all widget interaction, such as cancelling calls and disabling webcams.
* @private
*/
_endWidgetActions() {
// HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
if (this._appFrame.current) {
// In practice we could just do `+= ''` to trick the browser
// into thinking the URL changed, however I can foresee this
// being optimized out by a browser. Instead, we'll just point
// the iframe at a page that is reasonably safe to use in the
// event the iframe doesn't wink away.
// This is relative to where the Riot instance is located.
this._appFrame.current.src = 'about:blank';
}
// Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this._persistKey);
}
/* If user has permission to modify widgets, delete the widget,
* otherwise revoke access for the widget to load in the user's browser
*/
@ -336,22 +379,11 @@ export default class AppTile extends React.Component {
}
this.setState({deleting: true});
// HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
if (this._appFrame.current) {
// In practice we could just do `+= ''` to trick the browser
// into thinking the URL changed, however I can foresee this
// being optimized out by a browser. Instead, we'll just point
// the iframe at a page that is reasonably safe to use in the
// event the iframe doesn't wink away.
// This is relative to where the Riot instance is located.
this._appFrame.current.src = 'about:blank';
}
this._endWidgetActions();
WidgetUtils.setRoomWidget(
this.props.room.roomId,
this.props.id,
this.props.app.id,
).catch((e) => {
console.error('Failed to delete widget', e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -369,7 +401,7 @@ export default class AppTile extends React.Component {
}
_onRevokeClicked() {
console.info("Revoke widget permissions - %s", this.props.id);
console.info("Revoke widget permissions - %s", this.props.app.id);
this._revokeWidgetPermission();
}
@ -380,10 +412,10 @@ export default class AppTile extends React.Component {
// Destroy the old widget messaging before starting it back up again. Some widgets
// have startup routines that run when they are loaded, so we just need to reinitialize
// the messaging for them.
ActiveWidgetStore.delWidgetMessaging(this.props.id);
ActiveWidgetStore.delWidgetMessaging(this.props.app.id);
this._setupWidgetMessaging();
ActiveWidgetStore.setRoomId(this.props.id, this.props.room.roomId);
ActiveWidgetStore.setRoomId(this.props.app.id, this.props.room.roomId);
this.setState({loading: false});
}
@ -391,10 +423,10 @@ export default class AppTile extends React.Component {
// FIXME: There's probably no reason to do this here: it should probably be done entirely
// in ActiveWidgetStore.
const widgetMessaging = new WidgetMessaging(
this.props.id, this.props.url, this.props.userWidget, this._appFrame.current.contentWindow);
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
this.props.app.id, this._getRenderedUrl(), this.props.userWidget, this._appFrame.current.contentWindow);
ActiveWidgetStore.setWidgetMessaging(this.props.app.id, widgetMessaging);
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
console.log(`Widget ${this.props.app.id} requested capabilities: ` + requestedCapabilities);
requestedCapabilities = requestedCapabilities || [];
// Allow whitelisted capabilities
@ -406,7 +438,7 @@ export default class AppTile extends React.Component {
}, this.props.whitelistCapabilities);
if (requestedWhitelistCapabilies.length > 0 ) {
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties: ` +
console.log(`Widget ${this.props.app.id} allowing requested, whitelisted properties: ` +
requestedWhitelistCapabilies,
);
}
@ -414,18 +446,24 @@ export default class AppTile extends React.Component {
// TODO -- Add UI to warn about and optionally allow requested capabilities
ActiveWidgetStore.setWidgetCapabilities(this.props.id, requestedWhitelistCapabilies);
ActiveWidgetStore.setWidgetCapabilities(this.props.app.id, requestedWhitelistCapabilies);
if (this.props.onCapabilityRequest) {
this.props.onCapabilityRequest(requestedCapabilities);
}
// We only tell Jitsi widgets that we're ready because they're realistically the only ones
// using this custom extension to the widget API.
if (this.props.app.type === 'jitsi') {
widgetMessaging.flagReadyToContinue();
}
}).catch((err) => {
console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err);
console.log(`Failed to get capabilities for widget type ${this.props.app.type}`, this.props.app.id, err);
});
}
_onAction(payload) {
if (payload.widgetId === this.props.id) {
if (payload.widgetId === this.props.app.id) {
switch (payload.action) {
case 'm.sticker':
if (this._hasCapability('m.sticker')) {
@ -454,9 +492,9 @@ export default class AppTile extends React.Component {
_grantWidgetPermission() {
const roomId = this.props.room.roomId;
console.info("Granting permission for widget to load: " + this.props.eventId);
console.info("Granting permission for widget to load: " + this.props.app.eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId);
current[this.props.eventId] = true;
current[this.props.app.eventId] = true;
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
this.setState({hasPermissionToLoad: true});
@ -470,14 +508,14 @@ export default class AppTile extends React.Component {
_revokeWidgetPermission() {
const roomId = this.props.room.roomId;
console.info("Revoking permission for widget to load: " + this.props.eventId);
console.info("Revoking permission for widget to load: " + this.props.app.eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId);
current[this.props.eventId] = false;
current[this.props.app.eventId] = false;
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
this.setState({hasPermissionToLoad: false});
// Force the widget to be non-persistent (able to be deleted/forgotten)
ActiveWidgetStore.destroyPersistentWidget(this.props.id);
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
const PersistedElement = sdk.getComponent("elements.PersistedElement");
PersistedElement.destroyElement(this._persistKey);
}).catch(err => {
@ -488,8 +526,8 @@ export default class AppTile extends React.Component {
formatAppTileName() {
let appTileName = "No name";
if (this.props.name && this.props.name.trim()) {
appTileName = this.props.name.trim();
if (this.props.app.name && this.props.app.name.trim()) {
appTileName = this.props.app.name.trim();
}
return appTileName;
}
@ -506,6 +544,10 @@ export default class AppTile extends React.Component {
if (this.props.userWidget) {
this._onMinimiseClick();
} else {
if (this.props.show) {
// if we were being shown, end the widget as we're about to be minimized.
this._endWidgetActions();
}
dis.dispatch({
action: 'appsDrawer',
show: !this.props.show,
@ -513,20 +555,78 @@ export default class AppTile extends React.Component {
}
}
_getSafeUrl() {
const parsedWidgetUrl = url.parse(this.state.widgetUrl, true);
/**
* Replace the widget template variables in a url with their values
*
* @param {string} u The URL with template variables
*
* @returns {string} url with temlate variables replaced
*/
_templatedUrl(u) {
const myUserId = MatrixClientPeg.get().credentials.userId;
const myUser = MatrixClientPeg.get().getUser(myUserId);
const vars = Object.assign({
domain: "jitsi.riot.im", // v1 widgets have this hardcoded
}, this.props.app.data, {
'matrix_user_id': myUserId,
'matrix_room_id': this.props.room.roomId,
'matrix_display_name': myUser ? myUser.displayName : myUserId,
'matrix_avatar_url': myUser ? MatrixClientPeg.get().mxcUrlToHttp(myUser.avatarUrl) : '',
// TODO: Namespace themes through some standard
'theme': SettingsStore.getValue("theme"),
});
if (vars.conferenceId === undefined) {
// we'll need to parse the conference ID out of the URL for v1 Jitsi widgets
const parsedUrl = new URL(this.props.app.url);
vars.conferenceId = parsedUrl.searchParams.get("confId");
}
return uriFromTemplate(u, vars);
}
/**
* Get the URL used in the iframe
* In cases where we supply our own UI for a widget, this is an internal
* URL different to the one used if the widget is popped out to a separate
* tab / browser
*
* @returns {string} url
*/
_getRenderedUrl() {
let url;
if (this.props.app.type === 'jitsi') {
console.log("Replacing Jitsi widget URL with local wrapper");
url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true});
url = this._addWurlParams(url);
} else {
url = this._getSafeUrl(this.state.widgetUrl);
}
return this._templatedUrl(url);
}
_getPopoutUrl() {
if (this.props.app.type === 'jitsi') {
return this._templatedUrl(
WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: false}),
);
} else {
// use app.url, not state.widgetUrl, because we want the one without
// the wURL params for the popped-out version.
return this._templatedUrl(this._getSafeUrl(this.props.app.url));
}
}
_getSafeUrl(u) {
const parsedWidgetUrl = url.parse(u, true);
if (ENABLE_REACT_PERF) {
parsedWidgetUrl.search = null;
parsedWidgetUrl.query.react_perf = true;
}
let safeWidgetUrl = '';
if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol) || (
// Check if the widget URL is a Jitsi widget in Electron
parsedWidgetUrl.protocol === 'vector:'
&& parsedWidgetUrl.host === 'vector'
&& parsedWidgetUrl.pathname === '/webapp/jitsi.html'
&& this.props.type === 'jitsi'
)) {
if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol)) {
safeWidgetUrl = url.format(parsedWidgetUrl);
}
return safeWidgetUrl;
@ -556,9 +656,9 @@ export default class AppTile extends React.Component {
_onPopoutWidgetClick() {
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getSafeUrl(), '_blank', 'noopener=yes');
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
Object.assign(document.createElement('a'),
{ target: '_blank', href: this._getSafeUrl(), rel: 'noreferrer noopener'}).click();
{ target: '_blank', href: this._getPopoutUrl(), rel: 'noreferrer noopener'}).click();
}
_onReloadWidgetClick() {
@ -635,7 +735,7 @@ export default class AppTile extends React.Component {
<iframe
allow={iframeFeatures}
ref={this._appFrame}
src={this._getSafeUrl()}
src={this._getRenderedUrl()}
allowFullScreen={true}
sandbox={sandboxFlags}
onLoad={this._onLoaded} />
@ -700,7 +800,7 @@ export default class AppTile extends React.Component {
}
return <React.Fragment>
<div className={appTileClass} id={this.props.id}>
<div className={appTileClass} id={this.props.app.id}>
{ this.props.showMenubar &&
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
@ -747,12 +847,8 @@ export default class AppTile extends React.Component {
AppTile.displayName = 'AppTile';
AppTile.propTypes = {
id: PropTypes.string.isRequired,
eventId: PropTypes.string, // required for room widgets
url: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
app: PropTypes.object.isRequired,
room: PropTypes.object.isRequired,
type: PropTypes.string.isRequired,
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
fullWidth: PropTypes.bool,
@ -799,7 +895,6 @@ AppTile.propTypes = {
};
AppTile.defaultProps = {
url: "",
waitForIframeLoad: true,
showMenubar: true,
showTitle: true,

View file

@ -38,7 +38,7 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
const cli = MatrixClientPeg.get();
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
},

View file

@ -116,7 +116,8 @@ export default class Dropdown extends React.Component {
};
}
componentWillMount() {
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
this._button = createRef();
// Listen for all clicks on the document so we can close the
// menu when the user clicks somewhere else
@ -127,7 +128,8 @@ export default class Dropdown extends React.Component {
document.removeEventListener('click', this._onDocumentClick, false);
}
componentWillReceiveProps(nextProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
if (!nextProps.children || nextProps.children.length === 0) {
return;
}

View file

@ -121,7 +121,7 @@ export default class EditableItemList extends React.Component {
return (
<form onSubmit={this._onItemAdded} autoComplete="off"
noValidate={true} className="mx_EditableItemList_newItem">
<Field id={`mx_EditableItemList_new_${this.props.id}`} label={this.props.placeholder} type="text"
<Field label={this.props.placeholder} type="text"
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
list={this.props.suggestionsListId} />
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit">

View file

@ -62,7 +62,8 @@ export default createReactClass({
};
},
componentWillReceiveProps: function(nextProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(nextProps) {
if (nextProps.initialValue !== this.props.initialValue) {
this.value = nextProps.initialValue;
if (this._editable_div.current) {
@ -71,7 +72,8 @@ export default createReactClass({
}
},
componentWillMount: function() {
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
// we track value as an JS object field rather than in React state
// as React doesn't play nice with contentEditable.
this.value = '';

View file

@ -42,7 +42,7 @@ export default class EditableTextContainer extends React.Component {
this._onValueChanged = this._onValueChanged.bind(this);
}
componentWillMount() {
componentDidMount() {
if (this.props.getInitialValue === undefined) {
// use whatever was given in the initialValue property.
return;

View file

@ -20,6 +20,7 @@ import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler';
import {MatrixEvent, RoomMember} from "matrix-js-sdk";
import {useStateToggle} from "../../../hooks/useStateToggle";
import AccessibleButton from "./AccessibleButton";
const EventListSummary = ({events, children, threshold=3, onToggle, startExpanded, summaryMembers=[], summaryText}) => {
const [expanded, toggleExpanded] = useStateToggle(startExpanded);
@ -42,24 +43,15 @@ const EventListSummary = ({events, children, threshold=3, onToggle, startExpande
);
}
let body;
if (expanded) {
return (
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
<div className={"mx_EventListSummary_toggle"} onClick={toggleExpanded}>
{ _t('collapse') }
</div>
<div className="mx_EventListSummary_line">&nbsp;</div>
{ children }
</div>
);
}
const avatars = summaryMembers.map((m) => <MemberAvatar key={m.userId} member={m} width={14} height={14} />);
return (
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
<div className={"mx_EventListSummary_toggle"} onClick={toggleExpanded}>
{ _t('expand') }
</div>
body = <React.Fragment>
<div className="mx_EventListSummary_line">&nbsp;</div>
{ children }
</React.Fragment>;
} else {
const avatars = summaryMembers.map((m) => <MemberAvatar key={m.userId} member={m} width={14} height={14} />);
body = (
<div className="mx_EventTile_line">
<div className="mx_EventTile_info">
<span className="mx_EventListSummary_avatars" onClick={toggleExpanded}>
@ -70,6 +62,15 @@ const EventListSummary = ({events, children, threshold=3, onToggle, startExpande
</span>
</div>
</div>
);
}
return (
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
<AccessibleButton className="mx_EventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}>
{ expanded ? _t('collapse') : _t('expand') }
</AccessibleButton>
{ body }
</div>
);
};

View file

@ -23,10 +23,16 @@ import { debounce } from 'lodash';
// Invoke validation from user input (when typing, etc.) at most once every N ms.
const VALIDATION_THROTTLE_MS = 200;
const BASE_ID = "mx_Field";
let count = 1;
function getId() {
return `${BASE_ID}_${count++}`;
}
export default class Field extends React.PureComponent {
static propTypes = {
// The field's ID, which binds the input and label together.
id: PropTypes.string.isRequired,
// The field's ID, which binds the input and label together. Immutable.
id: PropTypes.string,
// The element to create. Defaults to "input".
// To define options for a select, use <Field><option ... /></Field>
element: PropTypes.oneOf(["input", "select", "textarea"]),
@ -63,13 +69,15 @@ export default class Field extends React.PureComponent {
// All other props pass through to the <input>.
};
constructor() {
super();
constructor(props) {
super(props);
this.state = {
valid: undefined,
feedback: undefined,
focused: false,
};
this.id = this.props.id || getId();
}
onFocus = (ev) => {
@ -167,6 +175,7 @@ export default class Field extends React.PureComponent {
inputProps.type = inputProps.type || "text";
inputProps.ref = input => this.input = input;
inputProps.placeholder = inputProps.placeholder || inputProps.label;
inputProps.id = this.id; // this overwrites the id from props
inputProps.onFocus = this.onFocus;
inputProps.onChange = this.onChange;
@ -211,7 +220,7 @@ export default class Field extends React.PureComponent {
return <div className={fieldClasses}>
{prefixContainer}
{fieldInput}
<label htmlFor={this.props.id}>{this.props.label}</label>
<label htmlFor={this.id}>{this.props.label}</label>
{postfixContainer}
{fieldTooltip}
</div>;

View file

@ -81,7 +81,8 @@ export default class Flair extends React.Component {
this._unmounted = true;
}
componentWillReceiveProps(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
this._generateAvatars(newProps.groups);
}

View file

@ -24,8 +24,8 @@ import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler";
function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
if (language.value.toUpperCase() == query.toUpperCase()) return true;
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
if (language.value.toUpperCase() === query.toUpperCase()) return true;
return false;
}
@ -40,7 +40,7 @@ export default class LanguageDropdown extends React.Component {
};
}
componentWillMount() {
componentDidMount() {
languageHandler.getAllLanguagesFromJson().then((langs) => {
langs.sort(function(a, b) {
if (a.label < b.label) return -1;

View file

@ -113,10 +113,12 @@ export default class PersistedElement extends React.Component {
componentDidMount() {
this.updateChild();
this.renderApp();
}
componentDidUpdate() {
this.updateChild();
this.renderApp();
}
componentWillUnmount() {
@ -141,6 +143,14 @@ export default class PersistedElement extends React.Component {
this.updateChildVisibility(this.child, true);
}
renderApp() {
const content = <div ref={this.collectChild} style={this.props.style}>
{this.props.children}
</div>;
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
}
updateChildVisibility(child, visible) {
if (!child) return;
child.style.display = visible ? 'block' : 'none';
@ -160,12 +170,6 @@ export default class PersistedElement extends React.Component {
}
render() {
const content = <div ref={this.collectChild} style={this.props.style}>
{this.props.children}
</div>;
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
return <div ref={this.collectChildContainer}></div>;
}
}

View file

@ -1,6 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -33,7 +33,7 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
},
@ -75,11 +75,7 @@ export default createReactClass({
const AppTile = sdk.getComponent('elements.AppTile');
return <AppTile
key={app.id}
id={app.id}
eventId={app.eventId}
url={app.url}
name={app.name}
type={app.type}
app={app}
fullWidth={true}
room={persistentWidgetInRoom}
userId={MatrixClientPeg.get().credentials.userId}

View file

@ -82,7 +82,8 @@ const Pill = createReactClass({
};
},
async componentWillReceiveProps(nextProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
async UNSAFE_componentWillReceiveProps(nextProps) {
let resourceId;
let prefix;
@ -155,10 +156,12 @@ const Pill = createReactClass({
this.setState({resourceId, pillType, member, group, room});
},
componentWillMount() {
componentDidMount() {
this._unmounted = false;
this._matrixClient = MatrixClientPeg.get();
this.componentWillReceiveProps(this.props);
// eslint-disable-next-line new-cap
this.UNSAFE_componentWillReceiveProps(this.props); // HACK: We shouldn't be calling lifecycle functions ourselves.
},
componentWillUnmount() {

View file

@ -62,11 +62,13 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
// TODO: [REACT-WARNING] Move this to class constructor
this._initStateFromProps(this.props);
},
componentWillReceiveProps: function(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(newProps) {
this._initStateFromProps(newProps);
},
@ -132,7 +134,7 @@ export default createReactClass({
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
if (this.state.custom) {
picker = (
<Field id={`powerSelector_custom_${this.props.powerLevelKey}`} type="number"
<Field type="number"
label={label} max={this.props.maxValue}
onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} onChange={this.onCustomChange}
value={String(this.state.customValue)} disabled={this.props.disabled} />
@ -151,7 +153,7 @@ export default createReactClass({
});
picker = (
<Field id={`powerSelector_notCustom_${this.props.powerLevelKey}`} element="select"
<Field element="select"
label={label} onChange={this.onSelectChange}
value={String(this.state.selectValue)} disabled={this.props.disabled}>
{options}

View file

@ -184,7 +184,7 @@ export default class ReplyThread extends React.Component {
ref={ref} permalinkCreator={permalinkCreator} />;
}
componentWillMount() {
componentDidMount() {
this.unmounted = false;
this.room = this.context.getRoom(this.props.parentEv.getRoomId());
this.room.on("Room.redaction", this.onRoomRedaction);

View file

@ -23,7 +23,6 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
// Controlled form component wrapping Field for inputting a room alias scoped to a given domain
export default class RoomAliasField extends React.PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
domain: PropTypes.string.isRequired,
onChange: PropTypes.func,
value: PropTypes.string.isRequired,
@ -50,7 +49,6 @@ export default class RoomAliasField extends React.PureComponent {
className="mx_RoomAliasField"
prefix={poundSign}
postfix={domain}
id={this.props.id}
ref={ref => this._fieldRef = ref}
onValidate={this._onValidate}
placeholder={_t("e.g. my-room")}

View file

@ -31,7 +31,6 @@ export default class SyntaxHighlight extends React.Component {
}
// componentDidUpdate used here for reusability
// componentWillReceiveProps fires too early to call highlightBlock on.
componentDidUpdate() {
if (this._el) highlightBlock(this._el);
}

View file

@ -36,11 +36,9 @@ const TintableSvg = createReactClass({
idSequence: 0,
},
componentWillMount: function() {
this.fixups = [];
},
componentDidMount: function() {
this.fixups = [];
this.id = TintableSvg.idSequence++;
TintableSvg.mounts[this.id] = this;
},

View file

@ -35,6 +35,7 @@ export default createReactClass({
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._user_id_input = createRef();
},

View file

@ -17,95 +17,17 @@ limitations under the License.
import React from "react";
import PropTypes from "prop-types";
import {replaceableComponent} from "../../../../utils/replaceableComponent";
import {MatrixClientPeg} from "../../../../MatrixClientPeg";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {ToDeviceChannel} from "matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel";
import {decodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
import Spinner from "../Spinner";
import * as QRCode from "qrcode";
const CODE_VERSION = 0x02; // the version of binary QR codes we support
const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us
const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
@replaceableComponent("views.elements.crypto.VerificationQRCode")
export default class VerificationQRCode extends React.PureComponent {
static propTypes = {
prefix: PropTypes.string.isRequired,
version: PropTypes.number.isRequired,
mode: PropTypes.number.isRequired,
transactionId: PropTypes.string.isRequired, // or requestEventId
firstKeyB64: PropTypes.string.isRequired,
secondKeyB64: PropTypes.string.isRequired,
secretB64: PropTypes.string.isRequired,
qrCodeData: PropTypes.object.isRequired,
};
static async getPropsForRequest(verificationRequest: VerificationRequest) {
const cli = MatrixClientPeg.get();
const myUserId = cli.getUserId();
const otherUserId = verificationRequest.otherUserId;
let mode = MODE_VERIFY_OTHER_USER;
if (myUserId === otherUserId) {
// Mode changes depending on whether or not we trust the master cross signing key
const myTrust = cli.checkUserTrust(myUserId);
if (myTrust.isCrossSigningVerified()) {
mode = MODE_VERIFY_SELF_TRUSTED;
} else {
mode = MODE_VERIFY_SELF_UNTRUSTED;
}
}
const requestEvent = verificationRequest.requestEvent;
const transactionId = requestEvent.getId()
? requestEvent.getId()
: ToDeviceChannel.getTransactionId(requestEvent);
const qrProps = {
prefix: BINARY_PREFIX,
version: CODE_VERSION,
mode,
transactionId,
firstKeyB64: '', // worked out shortly
secondKeyB64: '', // worked out shortly
secretB64: verificationRequest.encodedSharedSecret,
};
const myCrossSigningInfo = cli.getStoredCrossSigningForUser(myUserId);
const myDevices = (await cli.getStoredDevicesForUser(myUserId)) || [];
if (mode === MODE_VERIFY_OTHER_USER) {
// First key is our master cross signing key
qrProps.firstKeyB64 = myCrossSigningInfo.getId("master");
// Second key is the other user's master cross signing key
const otherUserCrossSigningInfo = cli.getStoredCrossSigningForUser(otherUserId);
qrProps.secondKeyB64 = otherUserCrossSigningInfo.getId("master");
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
// First key is our master cross signing key
qrProps.firstKeyB64 = myCrossSigningInfo.getId("master");
// Second key is the other device's device key
const otherDevice = verificationRequest.targetDevice;
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
const device = myDevices.find(d => d.deviceId === otherDeviceId);
qrProps.secondKeyB64 = device.getFingerprint();
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
// First key is our device's key
qrProps.firstKeyB64 = cli.getDeviceEd25519Key();
// Second key is what we think our master cross signing key is
qrProps.secondKeyB64 = myCrossSigningInfo.getId("master");
}
return qrProps;
}
constructor(props) {
super(props);
this.state = {
dataUri: null,
};
@ -119,39 +41,8 @@ export default class VerificationQRCode extends React.PureComponent {
}
async generateQrCode() {
let buf = Buffer.alloc(0); // we'll concat our way through life
const appendByte = (b: number) => {
const tmpBuf = Buffer.from([b]);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendInt = (i: number) => {
const tmpBuf = Buffer.alloc(2);
tmpBuf.writeInt16BE(i, 0);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendStr = (s: string, enc: string, withLengthPrefix = true) => {
const tmpBuf = Buffer.from(s, enc);
if (withLengthPrefix) appendInt(tmpBuf.byteLength);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendEncBase64 = (b64: string) => {
const b = decodeBase64(b64);
const tmpBuf = Buffer.from(b);
buf = Buffer.concat([buf, tmpBuf]);
};
// Actually build the buffer for the QR code
appendStr(this.props.prefix, "ascii", false);
appendByte(this.props.version);
appendByte(this.props.mode);
appendStr(this.props.transactionId, "utf-8");
appendEncBase64(this.props.firstKeyB64);
appendEncBase64(this.props.secondKeyB64);
appendEncBase64(this.props.secretB64);
// Now actually assemble the QR code's data URI
const uri = await QRCode.toDataURL([{data: buf, mode: 'byte'}], {
const uri = await QRCode.toDataURL([{data: this.props.qrCodeData.buffer, mode: 'byte'}], {
errorCorrectionLevel: 'L', // we want it as trivial-looking as possible
});
this.setState({dataUri: uri});

View file

@ -22,6 +22,7 @@ import { _t } from '../../../languageHandler';
import * as recent from '../../../emojipicker/recent';
import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
export const CATEGORY_HEADER_HEIGHT = 22;
export const EMOJI_HEIGHT = 37;
@ -214,7 +215,7 @@ class EmojiPicker extends React.Component {
<div className="mx_EmojiPicker">
<Header categories={this.categories} defaultCategory="recent" onAnchorClick={this.scrollToCategory} />
<Search query={this.state.filter} onChange={this.onChangeFilter} />
<div className="mx_EmojiPicker_body" ref={this.bodyRef} onScroll={this.onScroll}>
<AutoHideScrollbar className="mx_EmojiPicker_body" wrappedRef={e => this.bodyRef.current = e} onScroll={this.onScroll}>
{this.categories.map(category => {
const emojis = this.memoizedDataByCategory[category.id];
const categoryElement = (<Category key={category.id} id={category.id} name={category.name}
@ -226,7 +227,7 @@ class EmojiPicker extends React.Component {
heightBefore += height;
return categoryElement;
})}
</div>
</AutoHideScrollbar>
{this.state.previewEmoji || !this.props.showQuickReactions
? <Preview emoji={this.state.previewEmoji} />
: <QuickReactions onClick={this.onClickEmoji} selectedEmojis={this.props.selectedEmojis} /> }

View file

@ -49,12 +49,13 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this._unmounted = false;
this._initGroupStore(this.props.groupId);
},
componentWillReceiveProps(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore(this.props.groupId);
this._initGroupStore(newProps.groupId);

View file

@ -47,7 +47,7 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this._unmounted = false;
this._initGroupStore(this.props.groupId);
},

View file

@ -36,7 +36,7 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this._initGroupStore(this.props.groupId);
},

View file

@ -47,11 +47,12 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this._initGroupStore(this.props.groupId);
},
componentWillReceiveProps(newProps) {
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore(this.props.groupId);
this._initGroupStore(newProps.groupId);

View file

@ -39,7 +39,7 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this._unmounted = false;
this._initGroupStore(this.props.groupId);
},

View file

@ -55,7 +55,7 @@ const GroupTile = createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
FlairStore.getGroupProfileCached(this.context, this.props.groupId).then((profile) => {
this.setState({profile});
}).catch((err) => {

View file

@ -34,7 +34,7 @@ export default createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this.context.getJoinedGroups().then((result) => {
this.setState({groups: result.groups || [], error: null});
}, (err) => {

View file

@ -170,6 +170,7 @@ export default createReactClass({
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._iframe = createRef();
this._dummyLink = createRef();

View file

@ -67,11 +67,6 @@ export default class MImageBody extends React.Component {
this._image = createRef();
}
componentWillMount() {
this.unmounted = false;
this.context.on('sync', this.onClientSync);
}
// FIXME: factor this out and aplpy it to MVideoBody and MAudioBody too!
onClientSync(syncState, prevState) {
if (this.unmounted) return;
@ -258,6 +253,9 @@ export default class MImageBody extends React.Component {
}
componentDidMount() {
this.unmounted = false;
this.context.on('sync', this.onClientSync);
const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) {
let thumbnailPromise = Promise.resolve(null);

View file

@ -47,6 +47,7 @@ export default createReactClass({
maxImageHeight: PropTypes.number,
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._body = createRef();
},

Some files were not shown because too many files have changed in this diff Show more