You've already forked matrix-react-sdk
mirror of
https://github.com/matrix-org/matrix-react-sdk.git
synced 2025-11-20 16:22:28 +03:00
Merge remote-tracking branch 'upstream/develop' into compact-reply-rendering
This commit is contained in:
@@ -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>);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,7 +350,7 @@ export const ContextMenuButton = ({ label, isExpanded, children, ...props }) =>
|
||||
};
|
||||
ContextMenuButton.propTypes = {
|
||||
...AccessibleButton.propTypes,
|
||||
label: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
isExpanded: PropTypes.bool.isRequired, // whether or not the context menu is currently open
|
||||
};
|
||||
|
||||
@@ -377,7 +377,6 @@ export const MenuGroup = ({children, label, ...props}) => {
|
||||
</div>;
|
||||
};
|
||||
MenuGroup.propTypes = {
|
||||
...AccessibleButton.propTypes,
|
||||
label: PropTypes.string.isRequired,
|
||||
className: PropTypes.string, // optional
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ class CustomRoomTagPanel extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this._tagStoreToken = CustomRoomTagStore.addListener(() => {
|
||||
this.setState({tags: CustomRoomTagStore.getSortedTags()});
|
||||
});
|
||||
|
||||
@@ -23,11 +23,11 @@ import PropTypes from 'prop-types';
|
||||
import request from 'browser-request';
|
||||
import { _t } from '../../languageHandler';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import classnames from 'classnames';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
||||
export default class EmbeddedPage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
@@ -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 });
|
||||
},
|
||||
);
|
||||
@@ -117,10 +126,9 @@ export default class EmbeddedPage extends React.PureComponent {
|
||||
</div>;
|
||||
|
||||
if (this.props.scrollbar) {
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
return <GeminiScrollbarWrapper autoshow={true} className={classes}>
|
||||
return <AutoHideScrollbar className={classes}>
|
||||
{content}
|
||||
</GeminiScrollbarWrapper>;
|
||||
</AutoHideScrollbar>;
|
||||
} else {
|
||||
return <div className={classes}>
|
||||
{content}
|
||||
|
||||
@@ -39,6 +39,7 @@ import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Perm
|
||||
import {Group} from "matrix-js-sdk";
|
||||
import {allSettled, sleep} from "../../utils/promise";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
||||
const LONG_DESC_PLACEHOLDER = _td(
|
||||
`<h1>HTML for your community's page</h1>
|
||||
@@ -423,28 +424,35 @@ export default createReactClass({
|
||||
membershipBusy: false,
|
||||
publicityBusy: false,
|
||||
inviterProfile: null,
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup,
|
||||
};
|
||||
},
|
||||
|
||||
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);
|
||||
this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||
dis.unregister(this._dispatcherRef);
|
||||
|
||||
// Remove RightPanelStore listener
|
||||
if (this._rightPanelStoreToken) {
|
||||
this._rightPanelStoreToken.remove();
|
||||
}
|
||||
},
|
||||
|
||||
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,
|
||||
@@ -454,6 +462,12 @@ export default createReactClass({
|
||||
}
|
||||
},
|
||||
|
||||
_onRightPanelStoreUpdate: function() {
|
||||
this.setState({
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup,
|
||||
});
|
||||
},
|
||||
|
||||
_onGroupMyMembership: function(group) {
|
||||
if (this._unmounted || group.groupId !== this.props.groupId) return;
|
||||
if (group.myMembership === 'leave') {
|
||||
@@ -554,10 +568,6 @@ export default createReactClass({
|
||||
GROUP_JOINPOLICY_INVITE,
|
||||
},
|
||||
});
|
||||
dis.dispatch({
|
||||
action: 'panel_disable',
|
||||
sideDisabled: true,
|
||||
});
|
||||
},
|
||||
|
||||
_onShareClick: function() {
|
||||
@@ -580,10 +590,6 @@ export default createReactClass({
|
||||
profileForm: null,
|
||||
});
|
||||
break;
|
||||
case 'after_right_panel_phase_change':
|
||||
// We don't keep state on the right panel, so just re-render to update
|
||||
this.forceUpdate();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -1173,7 +1179,6 @@ export default createReactClass({
|
||||
render: function() {
|
||||
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
|
||||
return <Spinner />;
|
||||
@@ -1299,9 +1304,7 @@ export default createReactClass({
|
||||
);
|
||||
}
|
||||
|
||||
const rightPanel = RightPanelStore.getSharedInstance().isOpenForGroup
|
||||
? <RightPanel groupId={this.props.groupId} />
|
||||
: undefined;
|
||||
const rightPanel = this.state.showRightPanel ? <RightPanel groupId={this.props.groupId} /> : undefined;
|
||||
|
||||
const headerClasses = {
|
||||
"mx_GroupView_header": true,
|
||||
@@ -1332,10 +1335,10 @@ export default createReactClass({
|
||||
<GroupHeaderButtons />
|
||||
</div>
|
||||
<MainSplit panel={rightPanel}>
|
||||
<GeminiScrollbarWrapper className="mx_GroupView_body">
|
||||
<AutoHideScrollbar className="mx_GroupView_body">
|
||||
{ this._getMembershipSection() }
|
||||
{ this._getGroupSection() }
|
||||
</GeminiScrollbarWrapper>
|
||||
</AutoHideScrollbar>
|
||||
</MainSplit>
|
||||
</main>
|
||||
);
|
||||
|
||||
66
src/components/structures/HomePage.tsx
Normal file
66
src/components/structures/HomePage.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -22,7 +22,7 @@ import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
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';
|
||||
@@ -32,13 +32,14 @@ import sessionStore from '../../stores/SessionStore';
|
||||
import {MatrixClientPeg} 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';
|
||||
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";
|
||||
// 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.
|
||||
@@ -93,7 +94,8 @@ const LoggedInView = createReactClass({
|
||||
this._loadResizerPreferences();
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
this._matrixClient = this.props.matrixClient;
|
||||
|
||||
@@ -337,13 +339,13 @@ const LoggedInView = createReactClass({
|
||||
|
||||
let handled = false;
|
||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey ||
|
||||
ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
|
||||
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey;
|
||||
const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.PAGE_UP:
|
||||
case Key.PAGE_DOWN:
|
||||
if (!hasModifier) {
|
||||
if (!hasModifier && !isModifier) {
|
||||
this._onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
}
|
||||
@@ -365,8 +367,6 @@ const LoggedInView = createReactClass({
|
||||
}
|
||||
break;
|
||||
case Key.BACKTICK:
|
||||
if (ev.key !== "`") break;
|
||||
|
||||
// Ideally this would be CTRL+P for "Profile", but that's
|
||||
// taken by the print dialog. CTRL+I for "Information"
|
||||
// was previously chosen but conflicted with italics in
|
||||
@@ -379,12 +379,43 @@ const LoggedInView = createReactClass({
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.SLASH:
|
||||
if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev)) {
|
||||
KeyboardShortcuts.toggleDialog();
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.ARROW_UP:
|
||||
case Key.ARROW_DOWN:
|
||||
if (ev.altKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
dis.dispatch({
|
||||
action: 'view_room_delta',
|
||||
delta: ev.key === Key.ARROW_UP ? -1 : 1,
|
||||
unread: ev.shiftKey,
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.PERIOD:
|
||||
if (ctrlCmdOnly && (this.props.page_type === "room_view" || this.props.page_type === "group_view")) {
|
||||
dis.dispatch({
|
||||
action: 'toggle_right_panel',
|
||||
type: this.props.page_type === "room_view" ? "room" : "group",
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
} else if (!hasModifier) {
|
||||
} else if (!isModifier && !ev.altKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
// The above condition is crafted to _allow_ characters with Shift
|
||||
// already pressed (but not the Shift key down itself).
|
||||
|
||||
const isClickShortcut = ev.target !== document.body &&
|
||||
(ev.key === Key.SPACE || ev.key === Key.ENTER);
|
||||
|
||||
@@ -507,7 +538,6 @@ const LoggedInView = createReactClass({
|
||||
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');
|
||||
@@ -546,13 +576,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:
|
||||
|
||||
@@ -93,14 +93,19 @@ export default class MainSplit extends React.Component {
|
||||
const bodyView = React.Children.only(this.props.children);
|
||||
const panelView = this.props.panel;
|
||||
|
||||
if (this.props.collapsedRhs || !panelView) {
|
||||
return bodyView;
|
||||
} else {
|
||||
return <div className="mx_MainSplit" ref={this._setResizeContainerRef}>
|
||||
{ bodyView }
|
||||
const hasResizer = !this.props.collapsedRhs && panelView;
|
||||
|
||||
let children;
|
||||
if (hasResizer) {
|
||||
children = <React.Fragment>
|
||||
<ResizeHandle reverse={true} />
|
||||
{ panelView }
|
||||
</div>;
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
return <div className="mx_MainSplit" ref={hasResizer ? this._setResizeContainerRef : undefined}>
|
||||
{ bodyView }
|
||||
{ children }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
@@ -600,9 +600,8 @@ export default createReactClass({
|
||||
break;
|
||||
case 'view_room_directory': {
|
||||
const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
|
||||
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
|
||||
config: this.props.config,
|
||||
}, 'mx_RoomDirectory_dialogWrapper');
|
||||
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {},
|
||||
'mx_RoomDirectory_dialogWrapper', false, true);
|
||||
|
||||
// View the welcome or home page if we need something to look at
|
||||
this._viewSomethingBehindModal();
|
||||
@@ -658,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,
|
||||
@@ -1495,26 +1495,40 @@ export default createReactClass({
|
||||
}
|
||||
});
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
cli.on("crypto.verification.request", request => {
|
||||
if (request.pending) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: 'verifreq_' + request.channel.transactionId,
|
||||
title: _t("Verification Request"),
|
||||
icon: "verification",
|
||||
props: {request},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cli.on("crypto.verification.start", (verifier) => {
|
||||
cli.on("crypto.keySignatureUploadFailure", (failures, source, continuation) => {
|
||||
const KeySignatureUploadFailedDialog =
|
||||
sdk.getComponent('views.dialogs.KeySignatureUploadFailedDialog');
|
||||
Modal.createTrackedDialog(
|
||||
'Failed to upload key signatures',
|
||||
'Failed to upload key signatures',
|
||||
KeySignatureUploadFailedDialog,
|
||||
{ failures, source, continuation });
|
||||
});
|
||||
|
||||
cli.on("crypto.verification.request", request => {
|
||||
const isFlagOn = SettingsStore.isFeatureEnabled("feature_cross_signing");
|
||||
|
||||
if (!isFlagOn && !request.channel.deviceId) {
|
||||
request.cancel({code: "m.invalid_message", reason: "This client has cross-signing disabled"});
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.verifier) {
|
||||
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
|
||||
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
|
||||
verifier,
|
||||
verifier: request.verifier,
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
});
|
||||
}
|
||||
} else if (request.pending) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: 'verifreq_' + request.channel.transactionId,
|
||||
title: request.isSelfVerification ? _t("Self-verification request") : _t("Verification Request"),
|
||||
icon: "verification",
|
||||
props: {request},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
priority: ToastStore.PRIORITY_REALTIME,
|
||||
});
|
||||
}
|
||||
});
|
||||
// Fire the tinter right on startup to ensure the default theme is applied
|
||||
// A later sync can/will correct the tint to be the right value for the user
|
||||
const colorScheme = SettingsStore.getValue("roomColor");
|
||||
@@ -1893,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1908,7 +1928,10 @@ export default createReactClass({
|
||||
// secret storage.
|
||||
SettingsStore.setFeatureEnabled("feature_cross_signing", true);
|
||||
this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY });
|
||||
} else if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
} else if (
|
||||
SettingsStore.isFeatureEnabled("feature_cross_signing") &&
|
||||
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
|
||||
) {
|
||||
// This will only work if the feature is set to 'enable' in the config,
|
||||
// since it's too early in the lifecycle for users to have turned the
|
||||
// labs flag on.
|
||||
@@ -2005,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 = (
|
||||
|
||||
@@ -523,7 +523,8 @@ export default class MessagePanel extends React.Component {
|
||||
// if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId &&
|
||||
(mxEv.getType() === prevEvent.getType() || eventTypeContinues) &&
|
||||
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
|
||||
haveTileForEvent(prevEvent) && (mxEv.getType() === prevEvent.getType() || eventTypeContinues) &&
|
||||
(mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) {
|
||||
continuation = true;
|
||||
}
|
||||
@@ -837,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())) {
|
||||
@@ -862,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;
|
||||
}
|
||||
@@ -949,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;
|
||||
@@ -970,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { _t } from '../../languageHandler';
|
||||
import dis from '../../dispatcher';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MyGroups',
|
||||
@@ -37,7 +38,7 @@ export default createReactClass({
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._fetch();
|
||||
},
|
||||
|
||||
@@ -62,8 +63,6 @@ export default createReactClass({
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
||||
const GroupTile = sdk.getComponent("groups.GroupTile");
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
|
||||
let content;
|
||||
let contentHeader;
|
||||
@@ -74,7 +73,7 @@ export default createReactClass({
|
||||
});
|
||||
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
||||
content = groupNodes.length > 0 ?
|
||||
<GeminiScrollbarWrapper>
|
||||
<AutoHideScrollbar className="mx_MyGroups_scrollable">
|
||||
<div className="mx_MyGroups_microcopy">
|
||||
<p>
|
||||
{ _t(
|
||||
@@ -93,7 +92,7 @@ export default createReactClass({
|
||||
<div className="mx_MyGroups_joinedGroups">
|
||||
{ groupNodes }
|
||||
</div>
|
||||
</GeminiScrollbarWrapper> :
|
||||
</AutoHideScrollbar> :
|
||||
<div className="mx_MyGroups_placeholder">
|
||||
{ _t(
|
||||
"You're not currently a member of any communities.",
|
||||
|
||||
@@ -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);
|
||||
@@ -182,6 +183,7 @@ export default class RightPanel extends React.Component {
|
||||
member: payload.member,
|
||||
event: payload.event,
|
||||
verificationRequest: payload.verificationRequest,
|
||||
verificationRequestPromise: payload.verificationRequestPromise,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -231,6 +233,7 @@ export default class RightPanel extends React.Component {
|
||||
onClose={onClose}
|
||||
phase={this.state.phase}
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
verificationRequestPromise={this.state.verificationRequestPromise}
|
||||
/>;
|
||||
} else {
|
||||
panel = <MemberInfo
|
||||
|
||||
@@ -28,6 +28,7 @@ import { _t } from '../../languageHandler';
|
||||
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
||||
import Analytics from '../../Analytics';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
|
||||
|
||||
const MAX_NAME_LENGTH = 80;
|
||||
const MAX_TOPIC_LENGTH = 160;
|
||||
@@ -40,30 +41,23 @@ export default createReactClass({
|
||||
displayName: 'RoomDirectory',
|
||||
|
||||
propTypes: {
|
||||
config: PropTypes.object,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
config: {},
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
publicRooms: [],
|
||||
loading: true,
|
||||
protocolsLoading: true,
|
||||
error: null,
|
||||
instanceId: null,
|
||||
includeAll: false,
|
||||
roomServer: null,
|
||||
instanceId: undefined,
|
||||
roomServer: MatrixClientPeg.getHomeserverName(),
|
||||
filterString: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this.nextBatch = null;
|
||||
this.filterTimeout = null;
|
||||
@@ -96,6 +90,8 @@ export default createReactClass({
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
this.refreshRoomList();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
@@ -130,10 +126,10 @@ export default createReactClass({
|
||||
if (my_server != MatrixClientPeg.getHomeserverName()) {
|
||||
opts.server = my_server;
|
||||
}
|
||||
if (this.state.instanceId) {
|
||||
opts.third_party_instance_id = this.state.instanceId;
|
||||
} else if (this.state.includeAll) {
|
||||
if (this.state.instanceId === ALL_ROOMS) {
|
||||
opts.include_all_networks = true;
|
||||
} else if (this.state.instanceId) {
|
||||
opts.third_party_instance_id = this.state.instanceId;
|
||||
}
|
||||
if (this.nextBatch) opts.since = this.nextBatch;
|
||||
if (my_filter_string) opts.filter = { generic_search_term: my_filter_string };
|
||||
@@ -247,7 +243,7 @@ export default createReactClass({
|
||||
}
|
||||
},
|
||||
|
||||
onOptionChange: function(server, instanceId, includeAll) {
|
||||
onOptionChange: function(server, instanceId) {
|
||||
// clear next batch so we don't try to load more rooms
|
||||
this.nextBatch = null;
|
||||
this.setState({
|
||||
@@ -257,7 +253,6 @@ export default createReactClass({
|
||||
publicRooms: [],
|
||||
roomServer: server,
|
||||
instanceId: instanceId,
|
||||
includeAll: includeAll,
|
||||
error: null,
|
||||
}, this.refreshRoomList);
|
||||
// We also refresh the room list each time even though this
|
||||
@@ -305,7 +300,7 @@ export default createReactClass({
|
||||
|
||||
onJoinFromSearchClick: function(alias) {
|
||||
// If we don't have a particular instance id selected, just show that rooms alias
|
||||
if (!this.state.instanceId) {
|
||||
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
||||
// If the user specified an alias without a domain, add on whichever server is selected
|
||||
// in the dropdown
|
||||
if (alias.indexOf(':') == -1) {
|
||||
@@ -406,6 +401,12 @@ export default createReactClass({
|
||||
// would normally decide what the name is.
|
||||
name: room.name || room_alias || _t('Unnamed room'),
|
||||
};
|
||||
|
||||
if (this.state.roomServer) {
|
||||
payload.opts = {
|
||||
viaServers: [this.state.roomServer],
|
||||
};
|
||||
}
|
||||
}
|
||||
// It's not really possible to join Matrix rooms by ID because the HS has no way to know
|
||||
// which servers to start querying. However, there's no other way to join rooms in
|
||||
@@ -587,7 +588,7 @@ export default createReactClass({
|
||||
}
|
||||
|
||||
let placeholder = _t('Find a room…');
|
||||
if (!this.state.instanceId) {
|
||||
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
||||
placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer});
|
||||
} else if (instance_expected_field_type) {
|
||||
placeholder = instance_expected_field_type.placeholder;
|
||||
@@ -604,10 +605,18 @@ export default createReactClass({
|
||||
listHeader = <div className="mx_RoomDirectory_listheader">
|
||||
<DirectorySearchBox
|
||||
className="mx_RoomDirectory_searchbox"
|
||||
onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinFromSearchClick}
|
||||
placeholder={placeholder} showJoinButton={showJoinButton}
|
||||
onChange={this.onFilterChange}
|
||||
onClear={this.onFilterClear}
|
||||
onJoinClick={this.onJoinFromSearchClick}
|
||||
placeholder={placeholder}
|
||||
showJoinButton={showJoinButton}
|
||||
/>
|
||||
<NetworkDropdown
|
||||
protocols={this.protocols}
|
||||
onOptionChange={this.onOptionChange}
|
||||
selectedServerName={this.state.roomServer}
|
||||
selectedInstanceId={this.state.instanceId}
|
||||
/>
|
||||
<NetworkDropdown config={this.props.config} protocols={this.protocols} onOptionChange={this.onOptionChange} />
|
||||
</div>;
|
||||
}
|
||||
const explanation =
|
||||
@@ -628,7 +637,7 @@ export default createReactClass({
|
||||
title={_t("Explore rooms")}
|
||||
>
|
||||
<div className="mx_RoomDirectory">
|
||||
<p>{explanation}</p>
|
||||
{explanation}
|
||||
<div className="mx_RoomDirectory_list">
|
||||
{listHeader}
|
||||
{content}
|
||||
|
||||
@@ -96,7 +96,7 @@ export default createReactClass({
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
|
||||
|
||||
|
||||
@@ -111,21 +111,30 @@ export default class RoomSubList extends React.PureComponent {
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
|
||||
// but this is no longer true, so we must do it here (and can apply the small
|
||||
// optimisation of checking that we care about the room being read).
|
||||
//
|
||||
// Ultimately we need to transition to a state pushing flow where something
|
||||
// explicitly notifies the components concerned that the notif count for a room
|
||||
// has change (e.g. a Flux store).
|
||||
if (payload.action === 'on_room_read' &&
|
||||
this.props.list.some((r) => r.roomId === payload.roomId)
|
||||
) {
|
||||
this.forceUpdate();
|
||||
switch (payload.action) {
|
||||
case 'on_room_read':
|
||||
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
|
||||
// but this is no longer true, so we must do it here (and can apply the small
|
||||
// optimisation of checking that we care about the room being read).
|
||||
//
|
||||
// Ultimately we need to transition to a state pushing flow where something
|
||||
// explicitly notifies the components concerned that the notif count for a room
|
||||
// has change (e.g. a Flux store).
|
||||
if (this.props.list.some((r) => r.roomId === payload.roomId)) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'view_room':
|
||||
if (this.state.hidden && !this.props.forceExpand && payload.show_room_tile &&
|
||||
this.props.list.some((r) => r.roomId === payload.room_id)
|
||||
) {
|
||||
this.toggle();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onClick = (ev) => {
|
||||
toggle = () => {
|
||||
if (this.isCollapsibleOnClick()) {
|
||||
// The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
|
||||
const isHidden = !this.state.hidden;
|
||||
@@ -138,6 +147,10 @@ export default class RoomSubList extends React.PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
onClick = (ev) => {
|
||||
this.toggle();
|
||||
};
|
||||
|
||||
onHeaderKeyDown = (ev) => {
|
||||
switch (ev.key) {
|
||||
case Key.ARROW_LEFT:
|
||||
@@ -180,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)),
|
||||
});
|
||||
|
||||
@@ -30,7 +30,6 @@ import classNames from 'classnames';
|
||||
import { _t } from '../../languageHandler';
|
||||
import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks';
|
||||
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import ContentMessages from '../../ContentMessages';
|
||||
import Modal from '../../Modal';
|
||||
import * as sdk from '../../index';
|
||||
@@ -55,6 +54,8 @@ import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
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() {};
|
||||
@@ -97,8 +98,12 @@ export default createReactClass({
|
||||
viaServers: PropTypes.arrayOf(PropTypes.string),
|
||||
},
|
||||
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
const llMembers = MatrixClientPeg.get().hasLazyLoadMembersEnabled();
|
||||
const llMembers = this.context.hasLazyLoadMembersEnabled();
|
||||
return {
|
||||
room: null,
|
||||
roomId: null,
|
||||
@@ -131,6 +136,8 @@ export default createReactClass({
|
||||
isAlone: false,
|
||||
isPeeking: false,
|
||||
showingPinned: false,
|
||||
showReadReceipts: true,
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||
|
||||
// error object, as from the matrix client/server API
|
||||
// If we failed to load information about the room,
|
||||
@@ -161,29 +168,40 @@ 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);
|
||||
MatrixClientPeg.get().on("Room", this.onRoom);
|
||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
||||
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
||||
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
|
||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
MatrixClientPeg.get().on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.context.on("Room", this.onRoom);
|
||||
this.context.on("Room.timeline", this.onRoomTimeline);
|
||||
this.context.on("Room.name", this.onRoomName);
|
||||
this.context.on("Room.accountData", this.onRoomAccountData);
|
||||
this.context.on("RoomState.events", this.onRoomStateEvents);
|
||||
this.context.on("RoomState.members", this.onRoomStateMember);
|
||||
this.context.on("Room.myMembership", this.onMyMembership);
|
||||
this.context.on("accountData", this.onAccountData);
|
||||
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);
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
|
||||
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
|
||||
this._showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
|
||||
this._onReadReceiptsChange);
|
||||
|
||||
this._roomView = createRef();
|
||||
this._searchResultsPanel = createRef();
|
||||
},
|
||||
|
||||
_onReadReceiptsChange: function() {
|
||||
this.setState({
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId),
|
||||
});
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdate: function(initial) {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
@@ -204,8 +222,10 @@ export default createReactClass({
|
||||
return;
|
||||
}
|
||||
|
||||
const roomId = RoomViewStore.getRoomId();
|
||||
|
||||
const newState = {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
roomId,
|
||||
roomAlias: RoomViewStore.getRoomAlias(),
|
||||
roomLoading: RoomViewStore.isRoomLoading(),
|
||||
roomLoadError: RoomViewStore.getRoomLoadError(),
|
||||
@@ -214,9 +234,15 @@ export default createReactClass({
|
||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
shouldPeek: RoomViewStore.shouldPeek(),
|
||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", RoomViewStore.getRoomId()),
|
||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
|
||||
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:',
|
||||
@@ -231,7 +257,7 @@ export default createReactClass({
|
||||
// NB: This does assume that the roomID will not change for the lifetime of
|
||||
// the RoomView instance
|
||||
if (initial) {
|
||||
newState.room = MatrixClientPeg.get().getRoom(newState.roomId);
|
||||
newState.room = this.context.getRoom(newState.roomId);
|
||||
if (newState.room) {
|
||||
newState.showApps = this._shouldShowApps(newState.room);
|
||||
this._onRoomLoaded(newState.room);
|
||||
@@ -333,7 +359,7 @@ export default createReactClass({
|
||||
peekLoading: true,
|
||||
isPeeking: true, // this will change to false if peeking fails
|
||||
});
|
||||
MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
|
||||
this.context.peekInRoom(roomId).then((room) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
@@ -366,7 +392,7 @@ export default createReactClass({
|
||||
});
|
||||
} else if (room) {
|
||||
// Stop peeking because we have joined this room previously
|
||||
MatrixClientPeg.get().stopPeeking();
|
||||
this.context.stopPeeking();
|
||||
this.setState({isPeeking: false});
|
||||
}
|
||||
}
|
||||
@@ -405,21 +431,6 @@ export default createReactClass({
|
||||
this.onResize();
|
||||
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
|
||||
// XXX: EVIL HACK to autofocus inviting on empty rooms.
|
||||
// We use the setTimeout to avoid racing with focus_composer.
|
||||
if (this.state.room &&
|
||||
this.state.room.getJoinedMemberCount() == 1 &&
|
||||
this.state.room.getLiveTimeline() &&
|
||||
this.state.room.getLiveTimeline().getEvents() &&
|
||||
this.state.room.getLiveTimeline().getEvents().length <= 6) {
|
||||
const inviteBox = document.getElementById("mx_SearchableEntityList_query");
|
||||
setTimeout(function() {
|
||||
if (inviteBox) {
|
||||
inviteBox.focus();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
@@ -463,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();
|
||||
|
||||
@@ -478,18 +493,19 @@ export default createReactClass({
|
||||
roomView.removeEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
}
|
||||
dis.unregister(this.dispatcherRef);
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||
MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
|
||||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||
MatrixClientPeg.get().removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
MatrixClientPeg.get().removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
if (this.context) {
|
||||
this.context.removeListener("Room", this.onRoom);
|
||||
this.context.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
this.context.removeListener("Room.name", this.onRoomName);
|
||||
this.context.removeListener("Room.accountData", this.onRoomAccountData);
|
||||
this.context.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
this.context.removeListener("Room.myMembership", this.onMyMembership);
|
||||
this.context.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
this.context.removeListener("accountData", this.onAccountData);
|
||||
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);
|
||||
@@ -503,9 +519,18 @@ export default createReactClass({
|
||||
if (this._roomStoreToken) {
|
||||
this._roomStoreToken.remove();
|
||||
}
|
||||
// Remove RightPanelStore listener
|
||||
if (this._rightPanelStoreToken) {
|
||||
this._rightPanelStoreToken.remove();
|
||||
}
|
||||
|
||||
WidgetEchoStore.removeListener('update', this._onWidgetEchoStoreUpdate);
|
||||
|
||||
if (this._showReadReceiptsWatchRef) {
|
||||
SettingsStore.unwatchSetting(this._showReadReceiptsWatchRef);
|
||||
this._showReadReceiptsWatchRef = null;
|
||||
}
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this._updateRoomMembers.cancelPendingCall();
|
||||
|
||||
@@ -514,6 +539,12 @@ export default createReactClass({
|
||||
// Tinter.tint(); // reset colourscheme
|
||||
},
|
||||
|
||||
_onRightPanelStoreUpdate: function() {
|
||||
this.setState({
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||
});
|
||||
},
|
||||
|
||||
onPageUnload(event) {
|
||||
if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) {
|
||||
return event.returnValue =
|
||||
@@ -524,7 +555,6 @@ export default createReactClass({
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
let handled = false;
|
||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||
@@ -553,10 +583,6 @@ export default createReactClass({
|
||||
|
||||
onAction: function(payload) {
|
||||
switch (payload.action) {
|
||||
case 'after_right_panel_phase_change':
|
||||
// We don't keep state on the right panel, so just re-render to update
|
||||
this.forceUpdate();
|
||||
break;
|
||||
case 'message_send_failed':
|
||||
case 'message_sent':
|
||||
this._checkIfAlone(this.state.room);
|
||||
@@ -568,9 +594,7 @@ export default createReactClass({
|
||||
payload.data.description || payload.data.name);
|
||||
break;
|
||||
case 'picture_snapshot':
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
[payload.file], this.state.room.roomId, MatrixClientPeg.get(),
|
||||
);
|
||||
ContentMessages.sharedInstance().sendContentListToRoom([payload.file], this.state.room.roomId, this.context);
|
||||
break;
|
||||
case 'notifier_enabled':
|
||||
case 'upload_started':
|
||||
@@ -659,7 +683,7 @@ export default createReactClass({
|
||||
// we'll only be showing a spinner.
|
||||
if (this.state.joining) return;
|
||||
|
||||
if (ev.getSender() !== MatrixClientPeg.get().credentials.userId) {
|
||||
if (ev.getSender() !== this.context.credentials.userId) {
|
||||
// update unread count when scrolled up
|
||||
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
||||
// no change
|
||||
@@ -715,8 +739,7 @@ export default createReactClass({
|
||||
|
||||
_loadMembersIfJoined: async function(room) {
|
||||
// lazy load members if enabled
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
if (this.context.hasLazyLoadMembersEnabled()) {
|
||||
if (room && room.getMyMembership() === 'join') {
|
||||
try {
|
||||
await room.loadMembersIfNeeded();
|
||||
@@ -751,7 +774,7 @@ export default createReactClass({
|
||||
|
||||
_updatePreviewUrlVisibility: function({roomId}) {
|
||||
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit
|
||||
const key = MatrixClientPeg.get().isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
|
||||
const key = this.context.isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
|
||||
this.setState({
|
||||
showUrlPreview: SettingsStore.getValue(key, roomId),
|
||||
});
|
||||
@@ -784,12 +807,18 @@ export default createReactClass({
|
||||
this._updateE2EStatus(room);
|
||||
},
|
||||
|
||||
onCrossSigningKeysChanged: function() {
|
||||
const room = this.state.room;
|
||||
if (room) {
|
||||
this._updateE2EStatus(room);
|
||||
}
|
||||
},
|
||||
|
||||
_updateE2EStatus: async function(room) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli.isRoomEncrypted(room.roomId)) {
|
||||
if (!this.context.isRoomEncrypted(room.roomId)) {
|
||||
return;
|
||||
}
|
||||
if (!cli.isCryptoEnabled()) {
|
||||
if (!this.context.isCryptoEnabled()) {
|
||||
// If crypto is not currently enabled, we aren't tracking devices at all,
|
||||
// so we don't know what the answer is. Let's error on the safe side and show
|
||||
// a warning for this case.
|
||||
@@ -808,40 +837,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 !== cli.getUserId())
|
||||
.forEach((userId) => {
|
||||
(cli.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, cli.getUserId()] : verified;
|
||||
for (const userId of targets) {
|
||||
const devices = await cli.getStoredDevicesForUser(userId);
|
||||
const anyDeviceNotVerified = devices.some(({deviceId}) => {
|
||||
return !cli.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),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -910,7 +908,7 @@ export default createReactClass({
|
||||
|
||||
_updatePermissions: function(room) {
|
||||
if (room) {
|
||||
const me = MatrixClientPeg.get().getUserId();
|
||||
const me = this.context.getUserId();
|
||||
const canReact = room.getMyMembership() === "join" && room.currentState.maySendEvent("m.reaction", me);
|
||||
const canReply = room.maySendMessage();
|
||||
|
||||
@@ -994,7 +992,7 @@ export default createReactClass({
|
||||
|
||||
if (this.state.searchResults.next_batch) {
|
||||
debuglog("requesting more search results");
|
||||
const searchPromise = MatrixClientPeg.get().backPaginateRoomEventsSearch(
|
||||
const searchPromise = this.context.backPaginateRoomEventsSearch(
|
||||
this.state.searchResults);
|
||||
return this._handleSearchResult(searchPromise);
|
||||
} else {
|
||||
@@ -1020,10 +1018,8 @@ export default createReactClass({
|
||||
},
|
||||
|
||||
onJoinButtonClicked: function(ev) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
// If the user is a ROU, allow them to transition to a PWLU
|
||||
if (cli && cli.isGuest()) {
|
||||
if (this.context && this.context.isGuest()) {
|
||||
// Join this room once the user has registered and logged in
|
||||
// (If we failed to peek, we may not have a valid room object.)
|
||||
dis.dispatch({
|
||||
@@ -1120,7 +1116,7 @@ export default createReactClass({
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
ev.dataTransfer.files, this.state.room.roomId, MatrixClientPeg.get(),
|
||||
ev.dataTransfer.files, this.state.room.roomId, this.context,
|
||||
);
|
||||
this.setState({ draggingFile: false });
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
@@ -1133,12 +1129,12 @@ export default createReactClass({
|
||||
},
|
||||
|
||||
injectSticker: function(url, info, text) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
if (this.context.isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
|
||||
ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get())
|
||||
ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, this.context)
|
||||
.then(undefined, (error) => {
|
||||
if (error.name === "UnknownDeviceError") {
|
||||
// Let the staus bar handle this
|
||||
@@ -1216,7 +1212,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 :(")),
|
||||
@@ -1229,12 +1225,9 @@ export default createReactClass({
|
||||
},
|
||||
|
||||
getSearchResultTiles: function() {
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
const SearchResultTile = sdk.getComponent('rooms.SearchResultTile');
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
// XXX: todo: merge overlapping results somehow?
|
||||
// XXX: why doesn't searching on name work?
|
||||
|
||||
@@ -1242,21 +1235,21 @@ export default createReactClass({
|
||||
|
||||
if (this.state.searchInProgress) {
|
||||
ret.push(<li key="search-spinner">
|
||||
<Spinner />
|
||||
</li>);
|
||||
<Spinner />
|
||||
</li>);
|
||||
}
|
||||
|
||||
if (!this.state.searchResults.next_batch) {
|
||||
if (this.state.searchResults.results.length == 0) {
|
||||
ret.push(<li key="search-top-marker">
|
||||
<h2 className="mx_RoomView_topMarker">{ _t("No results") }</h2>
|
||||
</li>,
|
||||
);
|
||||
<h2 className="mx_RoomView_topMarker">{ _t("No results") }</h2>
|
||||
</li>,
|
||||
);
|
||||
} else {
|
||||
ret.push(<li key="search-top-marker">
|
||||
<h2 className="mx_RoomView_topMarker">{ _t("No more results") }</h2>
|
||||
</li>,
|
||||
);
|
||||
<h2 className="mx_RoomView_topMarker">{ _t("No more results") }</h2>
|
||||
</li>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1276,7 +1269,7 @@ export default createReactClass({
|
||||
|
||||
const mxEv = result.context.getEvent();
|
||||
const roomId = mxEv.getRoomId();
|
||||
const room = cli.getRoom(roomId);
|
||||
const room = this.context.getRoom(roomId);
|
||||
|
||||
if (!haveTileForEvent(mxEv)) {
|
||||
// XXX: can this ever happen? It will make the result count
|
||||
@@ -1343,7 +1336,7 @@ export default createReactClass({
|
||||
},
|
||||
|
||||
onForgetClick: function() {
|
||||
MatrixClientPeg.get().forget(this.state.room.roomId).then(function() {
|
||||
this.context.forget(this.state.room.roomId).then(function() {
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
}, function(err) {
|
||||
const errCode = err.errcode || _t("unknown error code");
|
||||
@@ -1360,7 +1353,7 @@ export default createReactClass({
|
||||
this.setState({
|
||||
rejecting: true,
|
||||
});
|
||||
MatrixClientPeg.get().leave(this.state.roomId).then(function() {
|
||||
this.context.leave(this.state.roomId).then(function() {
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
self.setState({
|
||||
rejecting: false,
|
||||
@@ -1387,15 +1380,14 @@ export default createReactClass({
|
||||
rejecting: true,
|
||||
});
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
try {
|
||||
const myMember = this.state.room.getMember(cli.getUserId());
|
||||
const myMember = this.state.room.getMember(this.context.getUserId());
|
||||
const inviteEvent = myMember.events.member;
|
||||
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
||||
const ignoredUsers = this.context.getIgnoredUsers();
|
||||
ignoredUsers.push(inviteEvent.getSender()); // de-duped internally in the js-sdk
|
||||
await cli.setIgnoredUsers(ignoredUsers);
|
||||
await this.context.setIgnoredUsers(ignoredUsers);
|
||||
|
||||
await cli.leave(this.state.roomId);
|
||||
await this.context.leave(this.state.roomId);
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
this.setState({
|
||||
rejecting: false,
|
||||
@@ -1614,7 +1606,7 @@ export default createReactClass({
|
||||
const createEvent = this.state.room.currentState.getStateEvents("m.room.create", "");
|
||||
if (!createEvent || !createEvent.getContent()['predecessor']) return null;
|
||||
|
||||
return MatrixClientPeg.get().getRoom(createEvent.getContent()['predecessor']['room_id']);
|
||||
return this.context.getRoom(createEvent.getContent()['predecessor']['room_id']);
|
||||
},
|
||||
|
||||
_getHiddenHighlightCount: function() {
|
||||
@@ -1708,7 +1700,7 @@ export default createReactClass({
|
||||
</ErrorBoundary>
|
||||
);
|
||||
} else {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const myUserId = this.context.credentials.userId;
|
||||
const myMember = this.state.room.getMember(myUserId);
|
||||
const inviteEvent = myMember.events.member;
|
||||
var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender();
|
||||
@@ -1775,13 +1767,13 @@ export default createReactClass({
|
||||
const showRoomUpgradeBar = (
|
||||
roomVersionRecommendation &&
|
||||
roomVersionRecommendation.needsUpgrade &&
|
||||
this.state.room.userMayUpgradeRoom(MatrixClientPeg.get().credentials.userId)
|
||||
this.state.room.userMayUpgradeRoom(this.context.credentials.userId)
|
||||
);
|
||||
|
||||
const showRoomRecoveryReminder = (
|
||||
SettingsStore.getValue("showRoomRecoveryReminder") &&
|
||||
MatrixClientPeg.get().isRoomEncrypted(this.state.room.roomId) &&
|
||||
!MatrixClientPeg.get().getKeyBackupEnabled()
|
||||
this.context.isRoomEncrypted(this.state.room.roomId) &&
|
||||
!this.context.getKeyBackupEnabled()
|
||||
);
|
||||
|
||||
const hiddenHighlightCount = this._getHiddenHighlightCount();
|
||||
@@ -1852,7 +1844,7 @@ export default createReactClass({
|
||||
const auxPanel = (
|
||||
<AuxPanel room={this.state.room}
|
||||
fullHeight={false}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
userId={this.context.credentials.userId}
|
||||
conferenceHandler={this.props.ConferenceHandler}
|
||||
draggingFile={this.state.draggingFile}
|
||||
displayConfCallNotification={this.state.displayConfCallNotification}
|
||||
@@ -1963,7 +1955,7 @@ export default createReactClass({
|
||||
<TimelinePanel
|
||||
ref={this._gatherTimelinePanelRef}
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
showReadReceipts={SettingsStore.getValue('showReadReceipts')}
|
||||
showReadReceipts={this.state.showReadReceipts}
|
||||
manageReadReceipts={!this.state.isPeeking}
|
||||
manageReadMarkers={!this.state.isPeeking}
|
||||
hidden={hideMessagePanel}
|
||||
@@ -2012,12 +2004,15 @@ export default createReactClass({
|
||||
},
|
||||
);
|
||||
|
||||
const showRightPanel = !forceHideRightPanel && this.state.room
|
||||
&& RightPanelStore.getSharedInstance().isOpenForRoom;
|
||||
const showRightPanel = !forceHideRightPanel && this.state.room && this.state.showRightPanel;
|
||||
const rightPanel = showRightPanel
|
||||
? <RightPanel roomId={this.state.room.roomId} resizeNotifier={this.props.resizeNotifier} />
|
||||
: null;
|
||||
|
||||
const timelineClasses = classNames("mx_RoomView_timeline", {
|
||||
mx_RoomView_timeline_rr_enabled: this.state.showReadReceipts,
|
||||
});
|
||||
|
||||
return (
|
||||
<RoomContext.Provider value={this.state}>
|
||||
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref={this._roomView}>
|
||||
@@ -2041,7 +2036,7 @@ export default createReactClass({
|
||||
>
|
||||
<div className={fadableSectionClasses}>
|
||||
{auxPanel}
|
||||
<div className="mx_RoomView_timeline">
|
||||
<div className={timelineClasses}>
|
||||
{topUnreadMessagesBar}
|
||||
{jumpToBottom}
|
||||
{messagePanel}
|
||||
|
||||
@@ -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) {
|
||||
@@ -782,7 +781,7 @@ export default createReactClass({
|
||||
if (!this._divScroll) {
|
||||
// Likewise, we should have the ref by this point, but if not
|
||||
// turn the NPE into something meaningful.
|
||||
throw new Error("ScrollPanel._getScrollNode called before gemini ref collected");
|
||||
throw new Error("ScrollPanel._getScrollNode called before AutoHideScrollbar ref collected");
|
||||
}
|
||||
|
||||
return this._divScroll;
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
Copyright 2019 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.
|
||||
@@ -18,41 +18,55 @@ limitations under the License.
|
||||
|
||||
import * as React from "react";
|
||||
import {_t} from '../../languageHandler';
|
||||
import PropTypes from "prop-types";
|
||||
import * as PropTypes from "prop-types";
|
||||
import * as sdk from "../../index";
|
||||
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||
import { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* Represents a tab for the TabbedView.
|
||||
*/
|
||||
export class Tab {
|
||||
public label: string;
|
||||
public icon: string;
|
||||
public body: React.ReactNode;
|
||||
|
||||
/**
|
||||
* Creates a new tab.
|
||||
* @param {string} tabLabel The untranslated tab label.
|
||||
* @param {string} tabIconClass The class for the tab icon. This should be a simple mask.
|
||||
* @param {string} tabJsx The JSX for the tab container.
|
||||
* @param {React.ReactNode} tabJsx The JSX for the tab container.
|
||||
*/
|
||||
constructor(tabLabel, tabIconClass, tabJsx) {
|
||||
constructor(tabLabel: string, tabIconClass: string, tabJsx: React.ReactNode) {
|
||||
this.label = tabLabel;
|
||||
this.icon = tabIconClass;
|
||||
this.body = tabJsx;
|
||||
}
|
||||
}
|
||||
|
||||
export default class TabbedView extends React.Component {
|
||||
interface IProps {
|
||||
tabs: Tab[];
|
||||
}
|
||||
|
||||
interface IState {
|
||||
activeTabIndex: number;
|
||||
}
|
||||
|
||||
export default class TabbedView extends React.Component<IProps, IState> {
|
||||
static propTypes = {
|
||||
// The tabs to show
|
||||
tabs: PropTypes.arrayOf(PropTypes.instanceOf(Tab)).isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
activeTabIndex: 0,
|
||||
};
|
||||
}
|
||||
|
||||
_getActiveTabIndex() {
|
||||
private _getActiveTabIndex() {
|
||||
if (!this.state || !this.state.activeTabIndex) return 0;
|
||||
return this.state.activeTabIndex;
|
||||
}
|
||||
@@ -62,7 +76,7 @@ export default class TabbedView extends React.Component {
|
||||
* @param {Tab} tab the tab to show
|
||||
* @private
|
||||
*/
|
||||
_setActiveTab(tab) {
|
||||
private _setActiveTab(tab: Tab) {
|
||||
const idx = this.props.tabs.indexOf(tab);
|
||||
if (idx !== -1) {
|
||||
this.setState({activeTabIndex: idx});
|
||||
@@ -71,7 +85,7 @@ export default class TabbedView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_renderTabLabel(tab) {
|
||||
private _renderTabLabel(tab: Tab) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let classes = "mx_TabbedView_tabLabel ";
|
||||
@@ -97,17 +111,17 @@ export default class TabbedView extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
_renderTabPanel(tab) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): React.ReactNode {
|
||||
const labels = this.props.tabs.map(tab => this._renderTabLabel(tab));
|
||||
const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]);
|
||||
|
||||
@@ -28,6 +28,7 @@ import { _t } from '../../languageHandler';
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
||||
const TagPanel = createReactClass({
|
||||
displayName: 'TagPanel',
|
||||
@@ -43,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);
|
||||
@@ -106,7 +107,6 @@ const TagPanel = createReactClass({
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
const tags = this.state.orderedTags.map((tag, index) => {
|
||||
return <DNDTagTile
|
||||
@@ -138,9 +138,8 @@ const TagPanel = createReactClass({
|
||||
{ clearButton }
|
||||
</div>
|
||||
<div className="mx_TagPanel_divider" />
|
||||
<GeminiScrollbarWrapper
|
||||
<AutoHideScrollbar
|
||||
className="mx_TagPanel_scroller"
|
||||
autoshow={true}
|
||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6253
|
||||
onMouseDown={this.onMouseDown}
|
||||
@@ -166,7 +165,7 @@ const TagPanel = createReactClass({
|
||||
</div>
|
||||
) }
|
||||
</Droppable>
|
||||
</GeminiScrollbarWrapper>
|
||||
</AutoHideScrollbar>
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export default class UserView extends React.Component {
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
if (this.props.userId) {
|
||||
this._loadProfileInfo();
|
||||
}
|
||||
|
||||
@@ -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,202 +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);
|
||||
}
|
||||
}
|
||||
|
||||
onStartClick = async () => {
|
||||
this.setState({
|
||||
phase: PHASE_BUSY,
|
||||
});
|
||||
const cli = MatrixClientPeg.get();
|
||||
const backupInfo = await cli.getKeyBackupVersion();
|
||||
this.setState({backupInfo});
|
||||
try {
|
||||
await accessSecretStorage(async () => {
|
||||
await cli.checkOwnCrossSigningTrust();
|
||||
if (backupInfo) await cli.restoreKeyBackupWithSecretStorage(backupInfo);
|
||||
});
|
||||
|
||||
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) {
|
||||
if (phase === PHASE_INTRO) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
||||
title = _t("Complete security");
|
||||
body = (
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Verify this session to grant it access to encrypted messages.",
|
||||
)}</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
onClick={this.onSkipClick}
|
||||
>
|
||||
{_t("Skip")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this.onStartClick}
|
||||
>
|
||||
{_t("Start")}
|
||||
</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 won’t 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}`);
|
||||
}
|
||||
@@ -241,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>
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
|
||||
196
src/components/structures/auth/SetupEncryptionBody.js
Normal file
196
src/components/structures/auth/SetupEncryptionBody.js
Normal 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 can’t 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 won’t 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 || ''}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ export default createReactClass({
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions);
|
||||
this._checkPermissions();
|
||||
},
|
||||
|
||||
@@ -82,7 +82,7 @@ export default createReactClass({
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
},
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export default class StatusMessageContextMenu extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
const { user } = this.props;
|
||||
if (!user) {
|
||||
return;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -86,7 +86,8 @@ export default createReactClass({
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount() {
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
},
|
||||
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -55,7 +55,8 @@ export default createReactClass({
|
||||
askReason: false,
|
||||
}),
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._reasonField = null;
|
||||
},
|
||||
|
||||
|
||||
@@ -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")) {
|
||||
createOpts.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 can’t disable this later. Bridges & most bots won’t 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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ export default createReactClass({
|
||||
button: PropTypes.string,
|
||||
onFinished: PropTypes.func,
|
||||
hasCloseButton: PropTypes.bool,
|
||||
onKeyDown: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@@ -50,10 +51,13 @@ export default createReactClass({
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
|
||||
<BaseDialog
|
||||
className="mx_InfoDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={this.props.hasCloseButton}
|
||||
onKeyDown={this.props.onKeyDown}
|
||||
>
|
||||
<div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
|
||||
{ this.props.description }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -35,6 +35,7 @@ import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore";
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
@@ -125,7 +126,7 @@ class ThreepidMember extends Member {
|
||||
class DMUserTile extends React.PureComponent {
|
||||
static propTypes = {
|
||||
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
|
||||
onRemove: PropTypes.func.isRequired, // takes 1 argument, the member being removed
|
||||
onRemove: PropTypes.func, // takes 1 argument, the member being removed
|
||||
};
|
||||
|
||||
_onRemove = (e) => {
|
||||
@@ -156,18 +157,25 @@ class DMUserTile extends React.PureComponent {
|
||||
width={avatarSize}
|
||||
height={avatarSize} />;
|
||||
|
||||
return (
|
||||
<span className='mx_InviteDialog_userTile'>
|
||||
<span className='mx_InviteDialog_userTile_pill'>
|
||||
{avatar}
|
||||
<span className='mx_InviteDialog_userTile_name'>{this.props.member.name}</span>
|
||||
</span>
|
||||
let closeButton;
|
||||
if (this.props.onRemove) {
|
||||
closeButton = (
|
||||
<AccessibleButton
|
||||
className='mx_InviteDialog_userTile_remove'
|
||||
onClick={this._onRemove}
|
||||
>
|
||||
<img src={require("../../../../res/img/icon-pill-remove.svg")} alt={_t('Remove')} width={8} height={8} />
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className='mx_InviteDialog_userTile'>
|
||||
<span className='mx_InviteDialog_userTile_pill'>
|
||||
{avatar}
|
||||
<span className='mx_InviteDialog_userTile_name'>{this.props.member.name}</span>
|
||||
</span>
|
||||
{ closeButton }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -640,11 +648,14 @@ export default class InviteDialog extends React.PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
_cancel = () => {
|
||||
// We do not want the user to close the dialog while an action is in progress
|
||||
if (this.state.busy) return;
|
||||
|
||||
this.props.onFinished();
|
||||
_onKeyDown = (e) => {
|
||||
// when the field is empty and the user hits backspace remove the right-most target
|
||||
if (!e.target.value && !this.state.busy && this.state.targets.length > 0 && e.key === Key.BACKSPACE &&
|
||||
!e.ctrlKey && !e.shiftKey && !e.metaKey
|
||||
) {
|
||||
e.preventDefault();
|
||||
this._removeMember(this.state.targets[this.state.targets.length - 1]);
|
||||
}
|
||||
};
|
||||
|
||||
_updateFilter = (e) => {
|
||||
@@ -889,7 +900,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||
_onManageSettingsClick = (e) => {
|
||||
e.preventDefault();
|
||||
dis.dispatch({ action: 'view_user_settings' });
|
||||
this._cancel();
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_renderSection(kind: "recents"|"suggestions") {
|
||||
@@ -984,17 +995,18 @@ export default class InviteDialog extends React.PureComponent {
|
||||
|
||||
_renderEditor() {
|
||||
const targets = this.state.targets.map(t => (
|
||||
<DMUserTile member={t} onRemove={this._removeMember} key={t.userId} />
|
||||
<DMUserTile member={t} onRemove={!this.state.busy && this._removeMember} key={t.userId} />
|
||||
));
|
||||
const input = (
|
||||
<textarea
|
||||
key={"input"}
|
||||
rows={1}
|
||||
onKeyDown={this._onKeyDown}
|
||||
onChange={this._updateFilter}
|
||||
value={this.state.filterText}
|
||||
ref={this._editorRef}
|
||||
onPaste={this._onPaste}
|
||||
autoFocus={true}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
@@ -1060,10 +1072,11 @@ export default class InviteDialog extends React.PureComponent {
|
||||
|
||||
title = _t("Direct Messages");
|
||||
helpText = _t(
|
||||
"If you can't find someone, ask them for their username, share your " +
|
||||
"username (%(userId)s) or <a>profile link</a>.",
|
||||
{userId},
|
||||
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{sub}</a>},
|
||||
"Start a conversation with someone using their name, username (like <userId/>) or email address.",
|
||||
{},
|
||||
{userId: () => {
|
||||
return <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>;
|
||||
}},
|
||||
);
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
@@ -1087,7 +1100,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||
<BaseDialog
|
||||
className='mx_InviteDialog'
|
||||
hasCancel={true}
|
||||
onFinished={this._cancel}
|
||||
onFinished={this.props.onFinished}
|
||||
title={title}
|
||||
>
|
||||
<div className='mx_InviteDialog_content'>
|
||||
|
||||
108
src/components/views/dialogs/KeySignatureUploadFailedDialog.js
Normal file
108
src/components/views/dialogs/KeySignatureUploadFailedDialog.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
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, {useState, useCallback, useRef} from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default function KeySignatureUploadFailedDialog({
|
||||
failures,
|
||||
source,
|
||||
continuation,
|
||||
onFinished,
|
||||
}) {
|
||||
const RETRIES = 2;
|
||||
const BaseDialog = sdk.getComponent('dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const [retry, setRetry] = useState(RETRIES);
|
||||
const [cancelled, setCancelled] = useState(false);
|
||||
const [retrying, setRetrying] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const onCancel = useRef(onFinished);
|
||||
|
||||
const causes = new Map([
|
||||
["_afterCrossSigningLocalKeyChange", _t("a new master key signature")],
|
||||
["checkOwnCrossSigningTrust", _t("a new cross-signing key signature")],
|
||||
["setDeviceVerification", _t("a device cross-signing signature")],
|
||||
]);
|
||||
const defaultCause = _t("a key signature");
|
||||
|
||||
const onRetry = useCallback(async () => {
|
||||
try {
|
||||
setRetrying(true);
|
||||
const cancel = new Promise((resolve, reject) => {
|
||||
onCancel.current = reject;
|
||||
}).finally(() => {
|
||||
setCancelled(true);
|
||||
});
|
||||
await Promise.race([
|
||||
continuation(),
|
||||
cancel,
|
||||
]);
|
||||
setSuccess(true);
|
||||
} catch (e) {
|
||||
setRetry(r => r-1);
|
||||
} finally {
|
||||
onCancel.current = onFinished;
|
||||
setRetrying(false);
|
||||
}
|
||||
}, [continuation, onFinished]);
|
||||
|
||||
let body;
|
||||
if (!success && !cancelled && continuation && retry > 0) {
|
||||
const reason = causes.get(source) || defaultCause;
|
||||
|
||||
body = (<div>
|
||||
<p>{_t("Riot encountered an error during upload of:")}</p>
|
||||
<p>{reason}</p>
|
||||
{retrying && <Spinner />}
|
||||
<pre>{JSON.stringify(failures, null, 2)}</pre>
|
||||
<DialogButtons
|
||||
primaryButton='Retry'
|
||||
hasCancel={true}
|
||||
onPrimaryButtonClick={onRetry}
|
||||
onCancel={onCancel.current}
|
||||
primaryDisabled={retrying}
|
||||
/>
|
||||
</div>);
|
||||
} else {
|
||||
body = (<div>
|
||||
{success ?
|
||||
<span>{_t("Upload completed")}</span> :
|
||||
cancelled ?
|
||||
<span>{_t("Cancelled signature upload")}</span> :
|
||||
<span>{_t("Unabled to upload")}</span>}
|
||||
<DialogButtons
|
||||
primaryButton={_t("OK")}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={onFinished}
|
||||
/>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
title={success ?
|
||||
_t("Signature upload success") :
|
||||
_t("Signature upload failed")}
|
||||
fixedWidth={false}
|
||||
onFinished={() => {}}
|
||||
>
|
||||
{body}
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default class LogoutDialog extends React.Component {
|
||||
defaultProps = {
|
||||
@@ -36,8 +35,8 @@ export default class LogoutDialog extends React.Component {
|
||||
this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this);
|
||||
this._onLogoutConfirm = this._onLogoutConfirm.bind(this);
|
||||
|
||||
const lowBandwidth = SettingsStore.getValue("lowBandwidth");
|
||||
const shouldLoadBackupStatus = !lowBandwidth && !MatrixClientPeg.get().getKeyBackupEnabled();
|
||||
const cli = MatrixClientPeg.get();
|
||||
const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled();
|
||||
|
||||
this.state = {
|
||||
shouldLoadBackupStatus: shouldLoadBackupStatus,
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 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.
|
||||
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 {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class ManualDeviceKeyVerificationDialog extends React.Component {
|
||||
static propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
device: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_onCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onLegacyFinished = (confirm) => {
|
||||
if (confirm) {
|
||||
MatrixClientPeg.get().setDeviceVerified(
|
||||
this.props.userId, this.props.device.deviceId, true,
|
||||
);
|
||||
}
|
||||
this.props.onFinished(confirm);
|
||||
}
|
||||
|
||||
render() {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
let text;
|
||||
if (MatrixClientPeg.get().getUserId() === this.props.userId) {
|
||||
text = _t("Confirm by comparing the following with the User Settings in your other session:");
|
||||
} else {
|
||||
text = _t("Confirm this user's session by comparing the following with their User Settings:");
|
||||
}
|
||||
|
||||
const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint());
|
||||
const body = (
|
||||
<div>
|
||||
<p>
|
||||
{ text }
|
||||
</p>
|
||||
<div className="mx_DeviceVerifyDialog_cryptoSection">
|
||||
<ul>
|
||||
<li><label>{ _t("Session name") }:</label> <span>{ this.props.device.getDisplayName() }</span></li>
|
||||
<li><label>{ _t("Session ID") }:</label> <span><code>{ this.props.device.deviceId }</code></span></li>
|
||||
<li><label>{ _t("Session key") }:</label> <span><code><b>{ key }</b></code></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
{ _t("If they don't match, the security of your communication may be compromised.") }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<QuestionDialog
|
||||
title={_t("Verify session")}
|
||||
description={body}
|
||||
button={_t("Verify session")}
|
||||
onFinished={this._onLegacyFinished}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -54,17 +54,18 @@ export default class NewSessionReviewDialog extends React.PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
onContinueClick = async () => {
|
||||
onContinueClick = () => {
|
||||
const { userId, device } = this.props;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const request = await cli.requestVerification(
|
||||
const requestPromise = cli.requestVerification(
|
||||
userId,
|
||||
[device.deviceId],
|
||||
);
|
||||
|
||||
this.props.onFinished(true);
|
||||
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
|
||||
verificationRequest: request,
|
||||
verificationRequestPromise: requestPromise,
|
||||
member: cli.getUser(userId),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export default createReactClass({
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
headerImage: PropTypes.string,
|
||||
quitOnly: PropTypes.bool, // quitOnly doesn't show the cancel button just the quit [x].
|
||||
fixedWidth: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@@ -63,11 +64,14 @@ export default createReactClass({
|
||||
primaryButtonClass = "danger";
|
||||
}
|
||||
return (
|
||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||
<BaseDialog
|
||||
className="mx_QuestionDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
headerImage={this.props.headerImage}
|
||||
hasCancel={this.props.hasCancelButton}
|
||||
fixedWidth={this.props.fixedWidth}
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ this.props.description }
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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} />,
|
||||
));
|
||||
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
29
src/components/views/dialogs/SetupEncryptionDialog.js
Normal file
29
src/components/views/dialogs/SetupEncryptionDialog.js
Normal 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>;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>);
|
||||
|
||||
@@ -18,6 +18,7 @@ import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import Field from "../elements/Field";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'TextInputDialog',
|
||||
@@ -28,9 +29,13 @@ export default createReactClass({
|
||||
PropTypes.string,
|
||||
]),
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
button: PropTypes.string,
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
hasCancel: PropTypes.bool,
|
||||
validator: PropTypes.func, // result of withValidation
|
||||
fixedWidth: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@@ -39,34 +44,71 @@ export default createReactClass({
|
||||
value: "",
|
||||
description: "",
|
||||
focus: true,
|
||||
hasCancel: true,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
value: this.props.value,
|
||||
valid: false,
|
||||
};
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._textinput = createRef();
|
||||
this._field = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.props.focus) {
|
||||
// Set the cursor at the end of the text input
|
||||
this._textinput.current.value = this.props.value;
|
||||
// this._field.current.value = this.props.value;
|
||||
this._field.current.focus();
|
||||
}
|
||||
},
|
||||
|
||||
onOk: function() {
|
||||
this.props.onFinished(true, this._textinput.current.value);
|
||||
onOk: async function(ev) {
|
||||
ev.preventDefault();
|
||||
if (this.props.validator) {
|
||||
await this._field.current.validate({ allowEmpty: false });
|
||||
|
||||
if (!this._field.current.state.valid) {
|
||||
this._field.current.focus();
|
||||
this._field.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.props.onFinished(true, this.state.value);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
onChange: function(ev) {
|
||||
this.setState({
|
||||
value: ev.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
onValidate: async function(fieldState) {
|
||||
const result = await this.props.validator(fieldState);
|
||||
this.setState({
|
||||
valid: result.valid,
|
||||
});
|
||||
return result;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
|
||||
<BaseDialog
|
||||
className="mx_TextInputDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
fixedWidth={this.props.fixedWidth}
|
||||
>
|
||||
<form onSubmit={this.onOk}>
|
||||
<div className="mx_Dialog_content">
|
||||
@@ -74,19 +116,25 @@ export default createReactClass({
|
||||
<label htmlFor="textinput"> { this.props.description } </label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
id="textinput"
|
||||
ref={this._textinput}
|
||||
<Field
|
||||
className="mx_TextInputDialog_input"
|
||||
defaultValue={this.props.value}
|
||||
autoFocus={this.props.focus}
|
||||
size="64" />
|
||||
ref={this._field}
|
||||
type="text"
|
||||
label={this.props.placeholder}
|
||||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
onValidate={this.props.validator ? this.onValidate : undefined}
|
||||
size="64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<DialogButtons primaryButton={this.props.button}
|
||||
<DialogButtons
|
||||
primaryButton={this.props.button}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
onCancel={this.onCancel} />
|
||||
onCancel={this.onCancel}
|
||||
hasCancel={this.props.hasCancel}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@ export default createReactClass({
|
||||
onSend: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged", this._onDeviceVerificationChanged);
|
||||
},
|
||||
|
||||
@@ -122,7 +122,6 @@ export default createReactClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
if (this.props.devices === null) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
@@ -168,7 +167,7 @@ export default createReactClass({
|
||||
title={_t('Room contains unknown sessions')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<GeminiScrollbarWrapper autoshow={false} className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<h4>
|
||||
{ _t('"%(RoomName)s" contains sessions that you haven\'t seen before.', {RoomName: this.props.room.name}) }
|
||||
</h4>
|
||||
@@ -176,7 +175,7 @@ export default createReactClass({
|
||||
{ _t("Unknown sessions") }:
|
||||
|
||||
<UnknownDeviceList devices={this.props.devices} />
|
||||
</GeminiScrollbarWrapper>
|
||||
</div>
|
||||
<DialogButtons primaryButton={sendButtonLabel}
|
||||
onPrimaryButtonClick={sendButtonOnClick}
|
||||
onCancel={this._onDismissClicked} />
|
||||
|
||||
@@ -22,34 +22,55 @@ import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class VerificationRequestDialog extends React.Component {
|
||||
static propTypes = {
|
||||
verificationRequest: PropTypes.object.isRequired,
|
||||
verificationRequest: PropTypes.object,
|
||||
verificationRequestPromise: PropTypes.object,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
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 ||
|
||||
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
|
||||
layout="dialog"
|
||||
verificationRequest={this.props.verificationRequest}
|
||||
verificationRequestPromise={this.props.verificationRequestPromise}
|
||||
onClose={this.props.onFinished}
|
||||
member={MatrixClientPeg.get().getUser(this.props.verificationRequest.otherUserId)}
|
||||
member={member}
|
||||
/>
|
||||
</BaseDialog>;
|
||||
}
|
||||
|
||||
onFinished() {
|
||||
this.props.verificationRequest.cancel();
|
||||
async onFinished() {
|
||||
this.props.onFinished();
|
||||
let request = this.props.verificationRequest;
|
||||
if (!request && this.props.verificationRequestPromise) {
|
||||
request = await this.props.verificationRequestPromise;
|
||||
}
|
||||
request.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||
// if false, will close the dialog as soon as the restore completes succesfully
|
||||
// default: true
|
||||
showSummary: PropTypes.bool,
|
||||
// If specified, gather the key from the user but then call the function with the backup
|
||||
// key rather than actually (necessarily) restoring the backup.
|
||||
keyCallback: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -103,9 +106,18 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||
restoreType: RESTORE_TYPE_PASSPHRASE,
|
||||
});
|
||||
try {
|
||||
// We do still restore the key backup: we must ensure that the key backup key
|
||||
// is the right one and restoring it is currently the only way we can do this.
|
||||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
|
||||
this.state.passPhrase, undefined, undefined, this.state.backupInfo,
|
||||
);
|
||||
if (this.props.keyCallback) {
|
||||
const key = await MatrixClientPeg.get().keyBackupKeyFromPassword(
|
||||
this.state.passPhrase, this.state.backupInfo,
|
||||
);
|
||||
this.props.keyCallback(key);
|
||||
}
|
||||
|
||||
if (!this.props.showSummary) {
|
||||
this.props.onFinished(true);
|
||||
return;
|
||||
@@ -135,6 +147,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
|
||||
this.state.recoveryKey, undefined, undefined, this.state.backupInfo,
|
||||
);
|
||||
if (this.props.keyCallback) {
|
||||
const key = MatrixClientPeg.get().keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
|
||||
this.props.keyCallback(key);
|
||||
}
|
||||
if (!this.props.showSummary) {
|
||||
this.props.onFinished(true);
|
||||
return;
|
||||
@@ -184,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,
|
||||
@@ -197,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();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket 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.
|
||||
@@ -15,241 +16,275 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {instanceForInstanceId} from '../../../utils/DirectoryUtils';
|
||||
import {
|
||||
ContextMenu,
|
||||
useContextMenu,
|
||||
ContextMenuButton,
|
||||
MenuItemRadio,
|
||||
MenuItem,
|
||||
MenuGroup,
|
||||
} from "../../structures/ContextMenu";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import {useSettingValue} from "../../../hooks/useSettings";
|
||||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import withValidation from "../elements/Validation";
|
||||
|
||||
const DEFAULT_ICON_URL = require("../../../../res/img/network-matrix.svg");
|
||||
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
||||
|
||||
export default class NetworkDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const SETTING_NAME = "room_directory_servers";
|
||||
|
||||
this.dropdownRootElement = null;
|
||||
this.ignoreEvent = null;
|
||||
const inPlaceOf = (elementRect) => ({
|
||||
right: window.innerWidth - elementRect.right,
|
||||
top: elementRect.top,
|
||||
chevronOffset: 0,
|
||||
chevronFace: "none",
|
||||
});
|
||||
|
||||
this.onInputClick = this.onInputClick.bind(this);
|
||||
this.onRootClick = this.onRootClick.bind(this);
|
||||
this.onDocumentClick = this.onDocumentClick.bind(this);
|
||||
this.onMenuOptionClick = this.onMenuOptionClick.bind(this);
|
||||
this.onInputKeyUp = this.onInputKeyUp.bind(this);
|
||||
this.collectRoot = this.collectRoot.bind(this);
|
||||
this.collectInputTextBox = this.collectInputTextBox.bind(this);
|
||||
const validServer = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: async ({ value }) => !!value,
|
||||
invalid: () => _t("Enter a server name"),
|
||||
}, {
|
||||
key: "available",
|
||||
final: true,
|
||||
test: async ({ value }) => {
|
||||
try {
|
||||
const opts = {
|
||||
limit: 1,
|
||||
server: value,
|
||||
};
|
||||
// check if we can successfully load this server's room directory
|
||||
await MatrixClientPeg.get().publicRooms(opts);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
valid: () => _t("Looks good"),
|
||||
invalid: () => _t("Can't find this server or its room list"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.inputTextBox = null;
|
||||
// This dropdown sources homeservers from three places:
|
||||
// + your currently connected homeserver
|
||||
// + homeservers in config.json["roomDirectory"]
|
||||
// + homeservers in SettingsStore["room_directory_servers"]
|
||||
// if a server exists in multiple, only keep the top-most entry.
|
||||
|
||||
const server = MatrixClientPeg.getHomeserverName();
|
||||
this.state = {
|
||||
expanded: false,
|
||||
selectedServer: server,
|
||||
selectedInstanceId: null,
|
||||
includeAllNetworks: false,
|
||||
const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, selectedInstanceId}) => {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
const _userDefinedServers = useSettingValue(SETTING_NAME);
|
||||
const [userDefinedServers, _setUserDefinedServers] = useState(_userDefinedServers);
|
||||
|
||||
const handlerFactory = (server, instanceId) => {
|
||||
return () => {
|
||||
onOptionChange(server, instanceId);
|
||||
closeMenu();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
// Listen for all clicks on the document so we can close the
|
||||
// menu when the user clicks somewhere else
|
||||
document.addEventListener('click', this.onDocumentClick, false);
|
||||
const setUserDefinedServers = servers => {
|
||||
_setUserDefinedServers(servers);
|
||||
SettingsStore.setValue(SETTING_NAME, null, "account", servers);
|
||||
};
|
||||
// keep local echo up to date with external changes
|
||||
useEffect(() => {
|
||||
_setUserDefinedServers(_userDefinedServers);
|
||||
}, [_userDefinedServers]);
|
||||
|
||||
// fire this now so the defaults can be set up
|
||||
const {selectedServer, selectedInstanceId, includeAllNetworks} = this.state;
|
||||
this.props.onOptionChange(selectedServer, selectedInstanceId, includeAllNetworks);
|
||||
}
|
||||
// we either show the button or the dropdown in its place.
|
||||
let content;
|
||||
if (menuDisplayed) {
|
||||
const config = SdkConfig.get();
|
||||
const roomDirectory = config.roomDirectory || {};
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.onDocumentClick, false);
|
||||
}
|
||||
const hsName = MatrixClientPeg.getHomeserverName();
|
||||
const configServers = new Set(roomDirectory.servers);
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.state.expanded && this.inputTextBox) {
|
||||
this.inputTextBox.focus();
|
||||
}
|
||||
}
|
||||
|
||||
onDocumentClick(ev) {
|
||||
// Close the dropdown if the user clicks anywhere that isn't
|
||||
// within our root element
|
||||
if (ev !== this.ignoreEvent) {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onRootClick(ev) {
|
||||
// This captures any clicks that happen within our elements,
|
||||
// such that we can then ignore them when they're seen by the
|
||||
// click listener on the document handler, ie. not close the
|
||||
// dropdown immediately after opening it.
|
||||
// NB. We can't just stopPropagation() because then the event
|
||||
// doesn't reach the React onClick().
|
||||
this.ignoreEvent = ev;
|
||||
}
|
||||
|
||||
onInputClick(ev) {
|
||||
this.setState({
|
||||
expanded: !this.state.expanded,
|
||||
});
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
onMenuOptionClick(server, instance, includeAll) {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
selectedServer: server,
|
||||
selectedInstanceId: instance ? instance.instance_id : null,
|
||||
includeAllNetworks: includeAll,
|
||||
});
|
||||
this.props.onOptionChange(server, instance ? instance.instance_id : null, includeAll);
|
||||
}
|
||||
|
||||
onInputKeyUp(e) {
|
||||
if (e.key === 'Enter') {
|
||||
this.setState({
|
||||
expanded: false,
|
||||
selectedServer: e.target.value,
|
||||
selectedNetwork: null,
|
||||
includeAllNetworks: false,
|
||||
});
|
||||
this.props.onOptionChange(e.target.value, null);
|
||||
}
|
||||
}
|
||||
|
||||
collectRoot(e) {
|
||||
if (this.dropdownRootElement) {
|
||||
this.dropdownRootElement.removeEventListener('click', this.onRootClick, false);
|
||||
}
|
||||
if (e) {
|
||||
e.addEventListener('click', this.onRootClick, false);
|
||||
}
|
||||
this.dropdownRootElement = e;
|
||||
}
|
||||
|
||||
collectInputTextBox(e) {
|
||||
this.inputTextBox = e;
|
||||
}
|
||||
|
||||
_getMenuOptions() {
|
||||
const options = [];
|
||||
const roomDirectory = this.props.config.roomDirectory || {};
|
||||
|
||||
let servers = [];
|
||||
if (roomDirectory.servers) {
|
||||
servers = servers.concat(roomDirectory.servers);
|
||||
}
|
||||
|
||||
if (!servers.includes(MatrixClientPeg.getHomeserverName())) {
|
||||
servers.unshift(MatrixClientPeg.getHomeserverName());
|
||||
}
|
||||
// configured servers take preference over user-defined ones, if one occurs in both ignore the latter one.
|
||||
const removableServers = new Set(userDefinedServers.filter(s => !configServers.has(s) && s !== hsName));
|
||||
const servers = [
|
||||
// we always show our connected HS, this takes precedence over it being configured or user-defined
|
||||
hsName,
|
||||
...Array.from(configServers).filter(s => s !== hsName).sort(),
|
||||
...Array.from(removableServers).sort(),
|
||||
];
|
||||
|
||||
// For our own HS, we can use the instance_ids given in the third party protocols
|
||||
// response to get the server to filter the room list by network for us.
|
||||
// We can't get thirdparty protocols for remote server yet though, so for those
|
||||
// we can only show the default room list.
|
||||
for (const server of servers) {
|
||||
options.push(this._makeMenuOption(server, null, true));
|
||||
if (server === MatrixClientPeg.getHomeserverName()) {
|
||||
options.push(this._makeMenuOption(server, null, false));
|
||||
if (this.props.protocols) {
|
||||
for (const proto of Object.keys(this.props.protocols)) {
|
||||
if (!this.props.protocols[proto].instances) continue;
|
||||
const options = servers.map(server => {
|
||||
const serverSelected = server === selectedServerName;
|
||||
const entries = [];
|
||||
|
||||
const sortedInstances = this.props.protocols[proto].instances;
|
||||
sortedInstances.sort(function(x, y) {
|
||||
const a = x.desc;
|
||||
const b = y.desc;
|
||||
if (a < b) {
|
||||
return -1;
|
||||
} else if (a > b) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
for (const instance of sortedInstances) {
|
||||
if (!instance.instance_id) continue;
|
||||
options.push(this._makeMenuOption(server, instance, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
const protocolsList = server === hsName ? Object.values(protocols) : [];
|
||||
if (protocolsList.length > 0) {
|
||||
// add a fake protocol with the ALL_ROOMS symbol
|
||||
protocolsList.push({
|
||||
instances: [{
|
||||
instance_id: ALL_ROOMS,
|
||||
desc: _t("All rooms"),
|
||||
}],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
protocolsList.forEach(({instances=[]}) => {
|
||||
[...instances].sort((b, a) => {
|
||||
return a.desc.localeCompare(b.desc);
|
||||
}).forEach(({desc, instance_id: instanceId}) => {
|
||||
entries.push(
|
||||
<MenuItemRadio
|
||||
key={String(instanceId)}
|
||||
active={serverSelected && instanceId === selectedInstanceId}
|
||||
onClick={handlerFactory(server, instanceId)}
|
||||
label={desc}
|
||||
className="mx_NetworkDropdown_server_network"
|
||||
>
|
||||
{ desc }
|
||||
</MenuItemRadio>);
|
||||
});
|
||||
});
|
||||
|
||||
_makeMenuOption(server, instance, includeAll, handleClicks) {
|
||||
if (handleClicks === undefined) handleClicks = true;
|
||||
let subtitle;
|
||||
if (server === hsName) {
|
||||
subtitle = (
|
||||
<div className="mx_NetworkDropdown_server_subtitle">
|
||||
{_t("Your server")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let icon;
|
||||
let name;
|
||||
let key;
|
||||
let removeButton;
|
||||
if (removableServers.has(server)) {
|
||||
const onClick = async () => {
|
||||
closeMenu();
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const {finished} = Modal.createTrackedDialog("Network Dropdown", "Remove server", QuestionDialog, {
|
||||
title: _t("Are you sure?"),
|
||||
description: _t("Are you sure you want to remove <b>%(serverName)s</b>", {
|
||||
serverName: server,
|
||||
}, {
|
||||
b: serverName => <b>{ serverName }</b>,
|
||||
}),
|
||||
button: _t("Remove"),
|
||||
fixedWidth: false,
|
||||
}, "mx_NetworkDropdown_dialog");
|
||||
|
||||
if (!instance && includeAll) {
|
||||
key = server;
|
||||
name = server;
|
||||
} else if (!instance) {
|
||||
key = server + '_all';
|
||||
name = 'Matrix';
|
||||
icon = <img src={require("../../../../res/img/network-matrix.svg")} />;
|
||||
} else {
|
||||
key = server + '_inst_' + instance.instance_id;
|
||||
const imgUrl = instance.icon ?
|
||||
MatrixClientPeg.get().mxcUrlToHttp(instance.icon, 25, 25, 'crop', true) :
|
||||
DEFAULT_ICON_URL;
|
||||
icon = <img src={imgUrl} />;
|
||||
name = instance.desc;
|
||||
}
|
||||
const [ok] = await finished;
|
||||
if (!ok) return;
|
||||
|
||||
const clickHandler = handleClicks ? this.onMenuOptionClick.bind(this, server, instance, includeAll) : null;
|
||||
// delete from setting
|
||||
setUserDefinedServers(servers.filter(s => s !== server));
|
||||
|
||||
return <div key={key} className="mx_NetworkDropdown_networkoption" onClick={clickHandler}>
|
||||
{icon}
|
||||
<span className="mx_NetworkDropdown_menu_network">{name}</span>
|
||||
</div>;
|
||||
}
|
||||
// the selected server is being removed, reset to our HS
|
||||
if (serverSelected === server) {
|
||||
onOptionChange(hsName, undefined);
|
||||
}
|
||||
};
|
||||
removeButton = <MenuItem onClick={onClick} label={_t("Remove server")} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
let currentValue;
|
||||
// ARIA: in actual fact the entire menu is one large radio group but for better screen reader support
|
||||
// we use group to notate server wrongly.
|
||||
return (
|
||||
<MenuGroup label={server} className="mx_NetworkDropdown_server" key={server}>
|
||||
<div className="mx_NetworkDropdown_server_title">
|
||||
{ server }
|
||||
{ removeButton }
|
||||
</div>
|
||||
{ subtitle }
|
||||
|
||||
let menu;
|
||||
if (this.state.expanded) {
|
||||
const menuOptions = this._getMenuOptions();
|
||||
menu = <div className="mx_NetworkDropdown_menu">
|
||||
{menuOptions}
|
||||
</div>;
|
||||
currentValue = <input type="text" className="mx_NetworkDropdown_networkoption"
|
||||
ref={this.collectInputTextBox} onKeyUp={this.onInputKeyUp}
|
||||
placeholder="matrix.org" // 'matrix.org' as an example of an HS name
|
||||
/>;
|
||||
} else {
|
||||
const instance = instanceForInstanceId(this.props.protocols, this.state.selectedInstanceId);
|
||||
currentValue = this._makeMenuOption(
|
||||
this.state.selectedServer, instance, this.state.includeAllNetworks, false,
|
||||
<MenuItemRadio
|
||||
active={serverSelected && !selectedInstanceId}
|
||||
onClick={handlerFactory(server, undefined)}
|
||||
label={_t("Matrix")}
|
||||
className="mx_NetworkDropdown_server_network"
|
||||
>
|
||||
{_t("Matrix")}
|
||||
</MenuItemRadio>
|
||||
{ entries }
|
||||
</MenuGroup>
|
||||
);
|
||||
});
|
||||
|
||||
const onClick = async () => {
|
||||
closeMenu();
|
||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
||||
const { finished } = Modal.createTrackedDialog("Network Dropdown", "Add a new server", TextInputDialog, {
|
||||
title: _t("Add a new server"),
|
||||
description: _t("Enter the name of a new server you want to explore."),
|
||||
button: _t("Add"),
|
||||
hasCancel: false,
|
||||
placeholder: _t("Server name"),
|
||||
validator: validServer,
|
||||
fixedWidth: false,
|
||||
}, "mx_NetworkDropdown_dialog");
|
||||
|
||||
const [ok, newServer] = await finished;
|
||||
if (!ok) return;
|
||||
|
||||
if (!userDefinedServers.includes(newServer)) {
|
||||
setUserDefinedServers([...userDefinedServers, newServer]);
|
||||
}
|
||||
|
||||
onOptionChange(newServer); // change filter to the new server
|
||||
};
|
||||
|
||||
const buttonRect = handle.current.getBoundingClientRect();
|
||||
content = <ContextMenu {...inPlaceOf(buttonRect)} onFinished={closeMenu}>
|
||||
<div className="mx_NetworkDropdown_menu">
|
||||
{options}
|
||||
<MenuItem className="mx_NetworkDropdown_server_add" label={undefined} onClick={onClick}>
|
||||
{_t("Add a new server...")}
|
||||
</MenuItem>
|
||||
</div>
|
||||
</ContextMenu>;
|
||||
} else {
|
||||
let currentValue;
|
||||
if (selectedInstanceId === ALL_ROOMS) {
|
||||
currentValue = _t("All rooms");
|
||||
} else if (selectedInstanceId) {
|
||||
const instance = instanceForInstanceId(protocols, selectedInstanceId);
|
||||
currentValue = _t("%(networkName)s rooms", {
|
||||
networkName: instance.desc,
|
||||
});
|
||||
} else {
|
||||
currentValue = _t("Matrix rooms");
|
||||
}
|
||||
|
||||
return <div className="mx_NetworkDropdown" ref={this.collectRoot}>
|
||||
<div className="mx_NetworkDropdown_input mx_no_textinput" onClick={this.onInputClick}>
|
||||
content = <ContextMenuButton
|
||||
className="mx_NetworkDropdown_handle"
|
||||
onClick={openMenu}
|
||||
isExpanded={menuDisplayed}
|
||||
>
|
||||
<span>
|
||||
{currentValue}
|
||||
<span className="mx_NetworkDropdown_arrow" />
|
||||
{menu}
|
||||
</div>
|
||||
</div>;
|
||||
</span> <span className="mx_NetworkDropdown_handle_server">
|
||||
({selectedServerName})
|
||||
</span>
|
||||
</ContextMenuButton>;
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="mx_NetworkDropdown" ref={handle}>
|
||||
{content}
|
||||
</div>;
|
||||
};
|
||||
|
||||
NetworkDropdown.propTypes = {
|
||||
onOptionChange: PropTypes.func.isRequired,
|
||||
protocols: PropTypes.object,
|
||||
// The room directory config. May have a 'servers' key that is a list of server names to include in the dropdown
|
||||
config: PropTypes.object,
|
||||
};
|
||||
|
||||
NetworkDropdown.defaultProps = {
|
||||
protocols: {},
|
||||
config: {},
|
||||
};
|
||||
export default NetworkDropdown;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,14 +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.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||
if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol)) {
|
||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
return safeWidgetUrl;
|
||||
@@ -550,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() {
|
||||
@@ -629,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} />
|
||||
@@ -694,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)}}>
|
||||
@@ -741,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,
|
||||
@@ -793,7 +895,6 @@ AppTile.propTypes = {
|
||||
};
|
||||
|
||||
AppTile.defaultProps = {
|
||||
url: "",
|
||||
waitForIframeLoad: true,
|
||||
showMenubar: true,
|
||||
showTitle: true,
|
||||
|
||||
@@ -38,7 +38,7 @@ export default createReactClass({
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -78,8 +78,7 @@ export class EditableItem extends React.Component {
|
||||
|
||||
return (
|
||||
<div className="mx_EditableItem">
|
||||
<img src={require("../../../../res/img/feather-customised/cancel.svg")} width={14} height={14}
|
||||
onClick={this._onRemove} className="mx_EditableItem_delete" alt={_t("Remove")} />
|
||||
<div onClick={this._onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
|
||||
<span className="mx_EditableItem_item">{this.props.value}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -122,9 +121,10 @@ 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"
|
||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged} />
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary">
|
||||
<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">
|
||||
{_t("Add")}
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"> </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"> </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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,15 +23,23 @@ 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"]),
|
||||
// The field's type (when used as an <input>). Defaults to "text".
|
||||
type: PropTypes.string,
|
||||
// id of a <datalist> element for suggestions
|
||||
list: PropTypes.string,
|
||||
// The field's label string.
|
||||
label: PropTypes.string,
|
||||
// The field's placeholder string. Defaults to the label.
|
||||
@@ -61,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) => {
|
||||
@@ -157,7 +167,7 @@ export default class Field extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
element, prefix, postfix, className, onValidate, children,
|
||||
tooltipContent, flagInvalid, tooltipClassName, ...inputProps} = this.props;
|
||||
tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props;
|
||||
|
||||
const inputElement = element || "input";
|
||||
|
||||
@@ -165,10 +175,12 @@ 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;
|
||||
inputProps.onBlur = this.onBlur;
|
||||
inputProps.list = list;
|
||||
|
||||
const fieldInput = React.createElement(inputElement, inputProps, children);
|
||||
|
||||
@@ -208,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>;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
Copyright 2018 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.
|
||||
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 GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
|
||||
function GeminiScrollbarWrapper(props) {
|
||||
const {wrappedRef, ...wrappedProps} = props;
|
||||
|
||||
// Enable forceGemini so that gemini is always enabled. This is
|
||||
// to avoid future issues where a feature is implemented without
|
||||
// doing QA on every OS/browser combination.
|
||||
//
|
||||
// By default GeminiScrollbar allows native scrollbars to be used
|
||||
// on macOS. Use forceGemini to enable Gemini's non-native
|
||||
// scrollbars on all OSs.
|
||||
return <GeminiScrollbar ref={wrappedRef} forceGemini={true} {...wrappedProps}>
|
||||
{ props.children }
|
||||
</GeminiScrollbar>;
|
||||
}
|
||||
export default GeminiScrollbarWrapper;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -183,7 +183,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);
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user